From 84af288f2c3637768237942ab16b491a8835d903 Mon Sep 17 00:00:00 2001 From: Eduarda Scharnhorst Date: Tue, 28 Jul 2026 15:03:03 +0100 Subject: [PATCH 1/2] feat: add mongodb-schema package and update dependencies - Introduced the @mongodb-js/mongodb-schema package to infer probabilistic schemas for MongoDB collections. - Updated package.json files across multiple packages to replace direct dependencies on 'mongodb-schema' with the new '@mongodb-js/mongodb-schema'. - Added ESLint, Prettier, and Mocha configuration files for consistent code quality and formatting. - Included a comprehensive Apache 2.0 License file. - Updated tests and autocompletion logic to utilize the new mongodb-schema package. --- .depalignrc.json | 3 +- .prettierignore | 1 + package-lock.json | 1320 +++++++++++---- packages/mongodb-schema/.depcheckrc | 13 + packages/mongodb-schema/.eslintignore | 6 + packages/mongodb-schema/.eslintrc.js | 8 + packages/mongodb-schema/.mocharc.js | 1 + packages/mongodb-schema/.prettierignore | 6 + packages/mongodb-schema/.prettierrc.json | 1 + packages/mongodb-schema/LICENSE | 201 +++ packages/mongodb-schema/README.md | 353 ++++ packages/mongodb-schema/bin/mongodb-schema | 231 +++ .../docs/mongodb-schema_diagram.graffle | Bin 0 -> 146874 bytes .../docs/mongodb-schema_diagram.png | Bin 0 -> 53480 bytes packages/mongodb-schema/examples/fanclub.json | 100 ++ .../examples/parse-from-file.ts | 58 + .../mongodb-schema/examples/parse-schema.ts | 27 + packages/mongodb-schema/package.json | 110 ++ .../mongodb-schema/scripts/time-testing.ts | 45 + packages/mongodb-schema/src/index.ts | 108 ++ .../mongodb-schema/src/schema-accessor.ts | 80 + .../mongodb-schema/src/schema-analyzer.ts | 775 +++++++++ .../internal-to-expanded.test.ts | 1283 ++++++++++++++ .../schema-converters/internal-to-expanded.ts | 151 ++ .../internal-to-mongodb.test.ts | 1403 +++++++++++++++ .../schema-converters/internal-to-mongodb.ts | 145 ++ .../internal-to-standard.test.ts | 1506 +++++++++++++++++ .../schema-converters/internal-to-standard.ts | 377 +++++ .../src/semantic-types/email.ts | 7 + .../src/semantic-types/index.ts | 5 + packages/mongodb-schema/src/stats.ts | 105 ++ .../mongodb-schema/src/to-typescript.spec.ts | 281 +++ packages/mongodb-schema/src/to-typescript.ts | 147 ++ packages/mongodb-schema/src/types.ts | 44 + packages/mongodb-schema/src/util.ts | 11 + .../test/all-bson-types-fixture.ts | 79 + .../test/all-bson-types.test.ts | 46 + .../test/array-object-types.test.ts | 104 ++ .../test/basic-embedded-array.test.ts | 57 + .../test/basic-embedded-documents.test.ts | 75 + .../test/basic-probability.test.ts | 36 + .../mongodb-schema/test/basic-unique.test.ts | 134 ++ packages/mongodb-schema/test/basic.test.ts | 91 + packages/mongodb-schema/test/bloated.test.ts | 142 ++ .../mongodb-schema/test/field-order.test.ts | 37 + .../test/get-schema-paths.test.ts | 113 ++ .../integration/generate-and-validate.test.ts | 235 +++ .../test/mixed-type-evolving-schema.test.ts | 81 + .../test/mixed-type-nested.test.ts | 101 ++ .../test/mixed-type-order.test.ts | 47 + .../test/mixed-type-probability.test.ts | 74 + .../test/nested-document-path.test.ts | 49 + packages/mongodb-schema/test/no-node.test.ts | 96 ++ packages/mongodb-schema/test/promise.test.ts | 56 + ...sion-strings-have-same-probability.test.ts | 37 + .../test/schema-accessor.test.ts | 61 + .../test/semantic-types.test.ts | 212 +++ .../test/simplified-schema.test.ts | 330 ++++ packages/mongodb-schema/test/stats.test.ts | 178 ++ packages/mongodb-schema/test/wrapper.test.ts | 36 + packages/mongodb-schema/tsconfig-lint.json | 5 + packages/mongodb-schema/tsconfig.json | 4 + packages/mongodb-ts-autocomplete/package.json | 2 +- .../src/autocompleter.spec.ts | 2 +- .../src/autocompleter.ts | 4 +- .../src/autocompletion-context.ts | 2 +- packages/mql-typescript/package.json | 2 +- .../src/driverSchema/docs-crawler.ts | 4 +- .../src/driverSchema/static-schemas.ts | 2 +- .../src/testGenerator/test-generator.ts | 2 +- 70 files changed, 11095 insertions(+), 353 deletions(-) create mode 100644 packages/mongodb-schema/.depcheckrc create mode 100644 packages/mongodb-schema/.eslintignore create mode 100644 packages/mongodb-schema/.eslintrc.js create mode 100644 packages/mongodb-schema/.mocharc.js create mode 100644 packages/mongodb-schema/.prettierignore create mode 100644 packages/mongodb-schema/.prettierrc.json create mode 100644 packages/mongodb-schema/LICENSE create mode 100644 packages/mongodb-schema/README.md create mode 100755 packages/mongodb-schema/bin/mongodb-schema create mode 100644 packages/mongodb-schema/docs/mongodb-schema_diagram.graffle create mode 100644 packages/mongodb-schema/docs/mongodb-schema_diagram.png create mode 100644 packages/mongodb-schema/examples/fanclub.json create mode 100644 packages/mongodb-schema/examples/parse-from-file.ts create mode 100644 packages/mongodb-schema/examples/parse-schema.ts create mode 100644 packages/mongodb-schema/package.json create mode 100644 packages/mongodb-schema/scripts/time-testing.ts create mode 100644 packages/mongodb-schema/src/index.ts create mode 100644 packages/mongodb-schema/src/schema-accessor.ts create mode 100644 packages/mongodb-schema/src/schema-analyzer.ts create mode 100644 packages/mongodb-schema/src/schema-converters/internal-to-expanded.test.ts create mode 100644 packages/mongodb-schema/src/schema-converters/internal-to-expanded.ts create mode 100644 packages/mongodb-schema/src/schema-converters/internal-to-mongodb.test.ts create mode 100644 packages/mongodb-schema/src/schema-converters/internal-to-mongodb.ts create mode 100644 packages/mongodb-schema/src/schema-converters/internal-to-standard.test.ts create mode 100644 packages/mongodb-schema/src/schema-converters/internal-to-standard.ts create mode 100644 packages/mongodb-schema/src/semantic-types/email.ts create mode 100644 packages/mongodb-schema/src/semantic-types/index.ts create mode 100644 packages/mongodb-schema/src/stats.ts create mode 100644 packages/mongodb-schema/src/to-typescript.spec.ts create mode 100644 packages/mongodb-schema/src/to-typescript.ts create mode 100644 packages/mongodb-schema/src/types.ts create mode 100644 packages/mongodb-schema/src/util.ts create mode 100644 packages/mongodb-schema/test/all-bson-types-fixture.ts create mode 100644 packages/mongodb-schema/test/all-bson-types.test.ts create mode 100644 packages/mongodb-schema/test/array-object-types.test.ts create mode 100644 packages/mongodb-schema/test/basic-embedded-array.test.ts create mode 100644 packages/mongodb-schema/test/basic-embedded-documents.test.ts create mode 100644 packages/mongodb-schema/test/basic-probability.test.ts create mode 100644 packages/mongodb-schema/test/basic-unique.test.ts create mode 100644 packages/mongodb-schema/test/basic.test.ts create mode 100644 packages/mongodb-schema/test/bloated.test.ts create mode 100644 packages/mongodb-schema/test/field-order.test.ts create mode 100644 packages/mongodb-schema/test/get-schema-paths.test.ts create mode 100644 packages/mongodb-schema/test/integration/generate-and-validate.test.ts create mode 100644 packages/mongodb-schema/test/mixed-type-evolving-schema.test.ts create mode 100644 packages/mongodb-schema/test/mixed-type-nested.test.ts create mode 100644 packages/mongodb-schema/test/mixed-type-order.test.ts create mode 100644 packages/mongodb-schema/test/mixed-type-probability.test.ts create mode 100644 packages/mongodb-schema/test/nested-document-path.test.ts create mode 100644 packages/mongodb-schema/test/no-node.test.ts create mode 100644 packages/mongodb-schema/test/promise.test.ts create mode 100644 packages/mongodb-schema/test/regression-strings-have-same-probability.test.ts create mode 100644 packages/mongodb-schema/test/schema-accessor.test.ts create mode 100644 packages/mongodb-schema/test/semantic-types.test.ts create mode 100644 packages/mongodb-schema/test/simplified-schema.test.ts create mode 100644 packages/mongodb-schema/test/stats.test.ts create mode 100644 packages/mongodb-schema/test/wrapper.test.ts create mode 100644 packages/mongodb-schema/tsconfig-lint.json create mode 100644 packages/mongodb-schema/tsconfig.json diff --git a/.depalignrc.json b/.depalignrc.json index 8aeaf9562..5b8e60e05 100644 --- a/.depalignrc.json +++ b/.depalignrc.json @@ -1,5 +1,6 @@ { "ignore": { - "node-fetch": ["^3.3.2", "^2.6.11"] + "node-fetch": ["^3.3.2", "^2.6.11"], + "js-yaml": ["^5.2.2", "^4.1.1"] } } diff --git a/.prettierignore b/.prettierignore index 73b9a5ffe..fe7b3b3b6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ resources/cidrs.json +packages/mongodb-schema/examples/fanclub.json diff --git a/package-lock.json b/package-lock.json index d7eb612cc..8ba5a1f13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6685,6 +6685,226 @@ "react": "^17.0.2" } }, + "node_modules/@mongodb-js/compass-test-server": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-test-server/-/compass-test-server-0.3.22.tgz", + "integrity": "sha512-iYoLWFePqjSgU0AW7I5EgOF0WKcm9Me4VWJYY4Nz5dN67S4JpiLA5YLF/7HMBPRwg0ymmPKOmEMknUUWoMUCZQ==", + "dev": true, + "license": "SSPL", + "dependencies": { + "mongodb-runner": "^5.8.0" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/@mongodb-js/mongodb-downloader": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.5.0.tgz", + "integrity": "sha512-N0ijRRNygm1xTQ3IhAMFcnZNhhF1CTwzFHqMk+tttm6LKD576t7j6olfuqSf8vFQ+tVpO6Z1JWOcquTKrpwOlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "decompress": "^4.2.1", + "mongodb-download-url": "^1.7.0", + "node-fetch": "^2.7.0", + "tar": "^6.1.15" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/mongodb": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/mongodb-runner": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.11.1.tgz", + "integrity": "sha512-vgp9pKqnfRr5aXUCYqh0DHw0I3LmRjD+LvoiGi2scp1BCgTeftSEZGy9py8xkzeWxCGBr7S0uwdmfcUPiVYusg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/mongodb-downloader": "^0.5.0", + "@mongodb-js/saslprep": "^1.3.2", + "debug": "^4.4.0", + "mongodb": "^6.9.0", + "mongodb-connection-string-url": "^3.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "mongodb-runner": "bin/runner.js" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@mongodb-js/compass-test-server/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/@mongodb-js/device-id": { "resolved": "packages/device-id", "link": true @@ -6729,6 +6949,10 @@ "resolved": "packages/mongodb-downloader", "link": true }, + "node_modules/@mongodb-js/mongodb-schema": { + "resolved": "packages/mongodb-schema", + "link": true + }, "node_modules/@mongodb-js/mongodb-server-log-checker": { "resolved": "packages/mongodb-server-log-checker", "link": true @@ -9706,6 +9930,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/reservoir": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/reservoir/-/reservoir-0.1.6.tgz", + "integrity": "sha512-2Eo3hcBaWYtrr0DwJLa8X/V/d8YQw72s7OuE9N+MrnYx2MQ72tftXX9lDQwnPKMeRiJrBXFbhfJ4FRMrs2+39w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -13101,44 +13332,6 @@ "node-fetch": "^2.7.0" } }, - "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/cross-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/cross-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/cross-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -21741,6 +21934,48 @@ "node": ">=10.5.0" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-machine-id": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", @@ -28467,32 +28702,6 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "packages/dl-center/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "packages/dl-center/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "packages/dl-center/node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -28500,22 +28709,6 @@ "dev": true, "license": "MIT" }, - "packages/dl-center/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "packages/dl-center/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "packages/download-url": { "name": "mongodb-download-url", "version": "1.8.15", @@ -28558,26 +28751,6 @@ "undici-types": ">=7.24.0 <7.24.7" } }, - "packages/download-url/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "packages/download-url/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -28590,12 +28763,6 @@ "node": ">=10" } }, - "packages/download-url/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "packages/download-url/node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -28603,22 +28770,6 @@ "dev": true, "license": "MIT" }, - "packages/download-url/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "packages/download-url/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "packages/get-os-info": { "name": "@mongodb-js/get-os-info", "version": "0.7.1", @@ -28874,31 +29025,6 @@ "@types/sinon": "*" } }, - "packages/mongodb-downloader/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "packages/mongodb-downloader/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "packages/mongodb-downloader/node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -28906,20 +29032,6 @@ "dev": true, "license": "MIT" }, - "packages/mongodb-downloader/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "packages/mongodb-downloader/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "packages/mongodb-log-writer": { "version": "2.5.14", "license": "Apache-2.0", @@ -29217,6 +29329,309 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, + "packages/mongodb-schema": { + "name": "@mongodb-js/mongodb-schema", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "reservoir": "^0.1.2" + }, + "bin": { + "mongodb-schema": "bin/mongodb-schema" + }, + "devDependencies": { + "@mongodb-js/compass-test-server": "^0.3.2", + "@mongodb-js/eslint-config-devtools": "^0.11.8", + "@mongodb-js/mocha-config-devtools": "^1.1.3", + "@mongodb-js/prettier-config-devtools": "^1.0.4", + "@mongodb-js/tsconfig-devtools": "^1.1.3", + "@types/chai": "^4.2.21", + "@types/json-schema": "^7.0.15", + "@types/mocha": "^9.1.1", + "@types/node": "^25.5.2", + "@types/reservoir": "^0.1.0", + "@types/sinon-chai": "^4.0.0", + "ajv": "^8.18.0", + "bson": "^4.6.3 || ^5 || ^6.10.3 || ^7.0.0", + "chai": "^4.5.0", + "depcheck": "^1.4.7", + "eslint": "^7.25.0 || ^8.0.0", + "gen-esm-wrapper": "^1.1.3", + "mocha": "^8.4.0", + "mongodb": "^7.5.0", + "nyc": "^15.1.0", + "prettier": "^3.8.1", + "sinon": "^9.2.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + }, + "optionalDependencies": { + "bson": "^4.6.3 || ^5 || ^6.10.3 || ^7.0.0", + "cli-table": "^0.3.4", + "js-yaml": "^5.2.2", + "mongodb": "^6.9.0 || ^7.0.0", + "mongodb-ns": "^3.2.1", + "numeral": "^2.0.6", + "progress": "^2.0.3", + "stats-lite": "^2.0.0", + "yargs": "^18.0.0" + } + }, + "packages/mongodb-schema/node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "packages/mongodb-schema/node_modules/@types/sinon-chai": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-4.0.0.tgz", + "integrity": "sha512-Uar+qk3TmeFsUWCwtqRNqNUE7vf34+MCJiQJR5M2rd4nCbhtE8RgTiHwN/mVwbfCjhmO6DiOel/MgzHkRMJJFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "packages/mongodb-schema/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/mongodb-schema/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "packages/mongodb-schema/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "packages/mongodb-schema/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0", + "optional": true + }, + "packages/mongodb-schema/node_modules/bson": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz", + "integrity": "sha512-h/C0qe6857pQhcSJHLfsR1uYGj98Ge3wKAD3Ed9KqH3wcVh+BM4Jq4xISD7vs9OPuT07n+q3QQVjslJ286j6ag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "packages/mongodb-schema/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "packages/mongodb-schema/node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/mongodb-schema/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT", + "optional": true + }, + "packages/mongodb-schema/node_modules/js-yaml": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz", + "integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "packages/mongodb-schema/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "packages/mongodb-schema/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "license": "MIT", + "optional": true, + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/mongodb-schema/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "packages/mongodb-schema/node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "packages/mongodb-schema/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "packages/mongodb-schema/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/mongodb-schema/node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "packages/mongodb-schema/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "optional": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "packages/mongodb-server-log-checker": { "name": "@mongodb-js/mongodb-server-log-checker", "version": "0.2.18", @@ -29275,11 +29690,11 @@ "version": "0.8.2", "license": "Apache-2.0", "dependencies": { + "@mongodb-js/mongodb-schema": "^0.1.0", "@mongodb-js/ts-autocomplete": "^0.5.13", "@mongosh/shell-api": "^5.1.6", "debug": "^4.4.0", "lodash": "^4.17.21", - "mongodb-schema": "^12.6.2", "node-cache": "^5.1.2", "typescript": "^5.9.3" }, @@ -29669,6 +30084,7 @@ }, "devDependencies": { "@mongodb-js/eslint-config-devtools": "^0.11.8", + "@mongodb-js/mongodb-schema": "^0.1.0", "@mongodb-js/prettier-config-devtools": "^1.0.4", "@mongodb-js/shell-bson-parser": "^1.5.14", "@mongodb-js/tsconfig-devtools": "^1.1.3", @@ -29682,7 +30098,6 @@ "js-yaml": "^4.1.1", "jsdom": "^24.1.3", "json-schema-to-zod": "^2.6.0", - "mongodb-schema": "^12.6.2", "prettier": "^3.8.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", @@ -29775,10 +30190,20 @@ "license": "MIT" }, "packages/mql-typescript/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -30528,26 +30953,6 @@ "node": ">=16 || 14 >=14.17" } }, - "packages/sbom-tools/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "packages/sbom-tools/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -30622,12 +31027,6 @@ "node": ">=10" } }, - "packages/sbom-tools/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "packages/sbom-tools/node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -30635,22 +31034,6 @@ "dev": true, "license": "MIT" }, - "packages/sbom-tools/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "packages/sbom-tools/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "packages/shell-bson-parser": { "name": "@mongodb-js/shell-bson-parser", "version": "1.5.14", @@ -36036,6 +36419,153 @@ "react-window": "^1.8.6" } }, + "@mongodb-js/compass-test-server": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-test-server/-/compass-test-server-0.3.22.tgz", + "integrity": "sha512-iYoLWFePqjSgU0AW7I5EgOF0WKcm9Me4VWJYY4Nz5dN67S4JpiLA5YLF/7HMBPRwg0ymmPKOmEMknUUWoMUCZQ==", + "dev": true, + "requires": { + "mongodb-runner": "^5.8.0" + }, + "dependencies": { + "@mongodb-js/mongodb-downloader": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.5.0.tgz", + "integrity": "sha512-N0ijRRNygm1xTQ3IhAMFcnZNhhF1CTwzFHqMk+tttm6LKD576t7j6olfuqSf8vFQ+tVpO6Z1JWOcquTKrpwOlQ==", + "dev": true, + "requires": { + "debug": "^4.4.0", + "decompress": "^4.2.1", + "mongodb-download-url": "^1.7.0", + "node-fetch": "^2.7.0", + "tar": "^6.1.15" + } + }, + "@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dev": true, + "requires": { + "@types/webidl-conversions": "*" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mongodb": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "dev": true, + "requires": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + } + }, + "mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dev": true, + "requires": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "mongodb-runner": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.11.1.tgz", + "integrity": "sha512-vgp9pKqnfRr5aXUCYqh0DHw0I3LmRjD+LvoiGi2scp1BCgTeftSEZGy9py8xkzeWxCGBr7S0uwdmfcUPiVYusg==", + "dev": true, + "requires": { + "@mongodb-js/mongodb-downloader": "^0.5.0", + "@mongodb-js/saslprep": "^1.3.2", + "debug": "^4.4.0", + "mongodb": "^6.9.0", + "mongodb-connection-string-url": "^3.0.0", + "yargs": "^17.7.2" + } + }, + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true + } + } + }, + "yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "@mongodb-js/device-id": { "version": "file:packages/device-id", "requires": { @@ -36302,38 +36832,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } } } }, @@ -36538,18 +37041,169 @@ "@types/sinon": "*" } }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true + } + } + }, + "@mongodb-js/mongodb-schema": { + "version": "file:packages/mongodb-schema", + "requires": { + "@mongodb-js/compass-test-server": "^0.3.2", + "@mongodb-js/eslint-config-devtools": "^0.11.8", + "@mongodb-js/mocha-config-devtools": "^1.1.3", + "@mongodb-js/prettier-config-devtools": "^1.0.4", + "@mongodb-js/tsconfig-devtools": "^1.1.3", + "@types/chai": "^4.2.21", + "@types/json-schema": "^7.0.15", + "@types/mocha": "^9.1.1", + "@types/node": "^25.5.2", + "@types/reservoir": "^0.1.0", + "@types/sinon-chai": "^4.0.0", + "ajv": "^8.18.0", + "bson": "^4.6.3 || ^5 || ^6.10.3 || ^7.0.0", + "chai": "^4.5.0", + "cli-table": "^0.3.4", + "depcheck": "^1.4.7", + "eslint": "^7.25.0 || ^8.0.0", + "gen-esm-wrapper": "^1.1.3", + "js-yaml": "^5.2.2", + "mocha": "^8.4.0", + "mongodb": "^7.5.0", + "mongodb-ns": "^3.2.1", + "numeral": "^2.0.6", + "nyc": "^15.1.0", + "prettier": "^3.8.1", + "progress": "^2.0.3", + "reservoir": "^0.1.2", + "sinon": "^9.2.3", + "stats-lite": "^2.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "yargs": "^18.0.0" + }, + "dependencies": { + "@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, "requires": { - "whatwg-url": "^5.0.0" + "undici-types": ">=7.24.0 <7.24.7" } }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "@types/sinon-chai": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-4.0.0.tgz", + "integrity": "sha512-Uar+qk3TmeFsUWCwtqRNqNUE7vf34+MCJiQJR5M2rd4nCbhtE8RgTiHwN/mVwbfCjhmO6DiOel/MgzHkRMJJFg==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "optional": true + }, + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "optional": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "optional": true + }, + "bson": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz", + "integrity": "sha512-h/C0qe6857pQhcSJHLfsR1uYGj98Ge3wKAD3Ed9KqH3wcVh+BM4Jq4xISD7vs9OPuT07n+q3QQVjslJ286j6ag==", + "dev": true + }, + "cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "optional": true, + "requires": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "dependencies": { + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "optional": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + } + } + }, + "emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "optional": true + }, + "js-yaml": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz", + "integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==", + "optional": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "optional": true, + "requires": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + } + }, + "strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "optional": true, + "requires": { + "ansi-regex": "^6.2.2" + } }, "undici-types": { "version": "7.24.6", @@ -36557,19 +37211,49 @@ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "optional": true, + "requires": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "dependencies": { + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "optional": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + } + } }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "optional": true, "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" } + }, + "yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "optional": true } } }, @@ -36628,6 +37312,7 @@ "requires": { "@mongodb-js/eslint-config-devtools": "^0.11.8", "@mongodb-js/mocha-config-devtools": "^1.1.3", + "@mongodb-js/mongodb-schema": "^0.1.0", "@mongodb-js/mql-typescript": "^0.6.2", "@mongodb-js/prettier-config-devtools": "^1.0.4", "@mongodb-js/ts-autocomplete": "^0.5.13", @@ -36646,7 +37331,6 @@ "lodash": "^4.17.21", "mocha": "^8.4.0", "mongodb": "^7.5.0", - "mongodb-schema": "^12.6.2", "node-cache": "^5.1.2", "nyc": "^15.1.0", "prettier": "^3.8.1", @@ -36884,6 +37568,7 @@ "version": "file:packages/mql-typescript", "requires": { "@mongodb-js/eslint-config-devtools": "^0.11.8", + "@mongodb-js/mongodb-schema": "^0.1.0", "@mongodb-js/prettier-config-devtools": "^1.0.4", "@mongodb-js/shell-bson-parser": "^1.5.14", "@mongodb-js/tsconfig-devtools": "^1.1.3", @@ -36899,7 +37584,6 @@ "jsdom": "^24.1.3", "json-schema-to-zod": "^2.6.0", "mongodb": "^7.5.0", - "mongodb-schema": "^12.6.2", "prettier": "^3.8.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", @@ -36965,9 +37649,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -37547,14 +38231,6 @@ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -37596,30 +38272,11 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==" }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } } } }, @@ -39906,6 +40563,12 @@ "integrity": "sha512-L0+4KOC47zH0AE/27YhzECRlE4HxHycQy0ng8P4SMmsYIdUqJFzKlk4LameozRQ2mRozPRwRAzpXsDsUgb6ezA==", "dev": true }, + "@types/reservoir": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/reservoir/-/reservoir-0.1.6.tgz", + "integrity": "sha512-2Eo3hcBaWYtrr0DwJLa8X/V/d8YQw72s7OuE9N+MrnYx2MQ72tftXX9lDQwnPKMeRiJrBXFbhfJ4FRMrs2+39w==", + "dev": true + }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -42327,35 +42990,6 @@ "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", "requires": { "node-fetch": "^2.7.0" - }, - "dependencies": { - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } } }, "cross-spawn": { @@ -48285,43 +48919,16 @@ "undici-types": ">=7.24.0 <7.24.7" } }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, "semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==" }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } } } }, @@ -48899,6 +49506,35 @@ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } + }, "node-machine-id": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", diff --git a/packages/mongodb-schema/.depcheckrc b/packages/mongodb-schema/.depcheckrc new file mode 100644 index 000000000..400223b76 --- /dev/null +++ b/packages/mongodb-schema/.depcheckrc @@ -0,0 +1,13 @@ +ignores: + - '@mongodb-js/prettier-config-devtools' + - '@mongodb-js/tsconfig-devtools' + - '@types/chai' + - '@types/json-schema' + - '@types/reservoir' + - '@types/sinon-chai' + - 'chai' + - 'json-schema' + - 'reservoir' + - 'sinon' +ignore-patterns: + - 'dist' diff --git a/packages/mongodb-schema/.eslintignore b/packages/mongodb-schema/.eslintignore new file mode 100644 index 000000000..2b1ce0295 --- /dev/null +++ b/packages/mongodb-schema/.eslintignore @@ -0,0 +1,6 @@ +.nyc_output +coverage +dist +docs +examples/fanclub.json +.esm-wrapper.mjs diff --git a/packages/mongodb-schema/.eslintrc.js b/packages/mongodb-schema/.eslintrc.js new file mode 100644 index 000000000..83296d735 --- /dev/null +++ b/packages/mongodb-schema/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + root: true, + extends: ['@mongodb-js/eslint-config-devtools'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig-lint.json'], + }, +}; diff --git a/packages/mongodb-schema/.mocharc.js b/packages/mongodb-schema/.mocharc.js new file mode 100644 index 000000000..64afeb1f9 --- /dev/null +++ b/packages/mongodb-schema/.mocharc.js @@ -0,0 +1 @@ +module.exports = require('@mongodb-js/mocha-config-devtools'); diff --git a/packages/mongodb-schema/.prettierignore b/packages/mongodb-schema/.prettierignore new file mode 100644 index 000000000..2b1ce0295 --- /dev/null +++ b/packages/mongodb-schema/.prettierignore @@ -0,0 +1,6 @@ +.nyc_output +coverage +dist +docs +examples/fanclub.json +.esm-wrapper.mjs diff --git a/packages/mongodb-schema/.prettierrc.json b/packages/mongodb-schema/.prettierrc.json new file mode 100644 index 000000000..dfae21d04 --- /dev/null +++ b/packages/mongodb-schema/.prettierrc.json @@ -0,0 +1 @@ +"@mongodb-js/prettier-config-devtools" diff --git a/packages/mongodb-schema/LICENSE b/packages/mongodb-schema/LICENSE new file mode 100644 index 000000000..5e0fd33cb --- /dev/null +++ b/packages/mongodb-schema/LICENSE @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "{}" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright {yyyy} {name of copyright owner} + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/mongodb-schema/README.md b/packages/mongodb-schema/README.md new file mode 100644 index 000000000..ee5eacf3e --- /dev/null +++ b/packages/mongodb-schema/README.md @@ -0,0 +1,353 @@ +# @mongodb-js/mongodb-schema [![][npm_img]][npm_url] + +Infer a probabilistic schema for a MongoDB collection. + +## Usage + +`mongodb-schema` can be used as a command line tool or programmatically in your application as a node module. + +### Command line + +To install mongodb-schema for command line use, run `npm install -g @mongodb-js/mongodb-schema`. This will add a new +shell script which you can run directly from the command line. + +The command line tool expects a MongoDB connection URI and a namespace in the form `.`. +Without further arguments, it will sample 100 random documents from the collection and print a schema of +the collection in JSON format to stdout. + +``` +mongodb-schema mongodb://localhost:27017 mongodb.fanclub +``` + +Additional arguments change the number of samples (`--number`), print additional statistics about the +schema analysis (`--stats`), switch to a different output format (`--format`), or let you suppress the +schema output altogether (`--no-output`) if you are only interested in the schema statistics, semantic +type discovery (`--semantic-types`), and the ability to disable value collection (`--no-values`). + +For more information, run + +``` +mongodb-schema --help +``` + +### API + +The following example demonstrates how `mongodb-schema` can be used programmatically from +your node application. You need to additionally install the MongoDB node driver to follow +along with this example. + +Make sure you have a `mongod` running on localhost on port 27017 (or change the example +below accordingly). + +1. From your application folder, install the driver and `mongodb-schema` locally: + + ``` + npm install --save mongodb @mongodb-js/mongodb-schema + ``` + +2. (optional) If you don't have any data in your MongoDB instance yet, you can create a + `test.data` collection with this command: + + ``` + mongosh --eval "db.data.insertMany([{_id: 1, a: true}, {_id: 2, a: 'true'}, {_id: 3, a: 1}, {_id: 4}])" localhost:27017/test + ``` + +3. Create a new file `parse-schema.js` and paste in the following code: + + ```javascript + const { parseSchema } = require('@mongodb-js/mongodb-schema'); + const { MongoClient } = require('mongodb'); + + const dbName = 'test'; + const uri = `mongodb://localhost:27017/${dbName}`; + const client = new MongoClient(uri); + + async function run() { + try { + const database = client.db(dbName); + const documentStream = database.collection('data').find(); + + // Here we are passing in a cursor as the first argument. You can + // also pass in a stream or an array of documents directly. + const schema = await parseSchema(documentStream); + + console.log(JSON.stringify(schema, null, 2)); + } finally { + await client.close(); + } + } + + run().catch(console.dir); + ``` + +4. When we run the above with `node ./parse-schema.js`, we'll see output + similar to this (some fields not present here for clarity): + +```javascript +{ + "count": 4, // parsed 4 documents + "fields": [ // an array of Field objects, @see `./src/schema-analyzer.ts` + { + "name": "_id", + "count": 4, // 4 documents counted with _id + "type": "Number", // the type of _id is `Number` + "probability": 1, // all documents had an _id field + "hasDuplicates": false, // therefore no duplicates + "types": [ // an array of Type objects, @see `./src/schema-analyzer.ts` + { + "name": "Number", // name of the type + "count": 4, // 4 numbers counted + "probability": 1, + "unique": 4, + "values": [ // array of encountered values + 1, + 2, + 3, + 4 + ] + } + ] + }, + { + "name": "a", + "count": 3, // only 3 documents with field `a` counted + "probability": 0.75, // hence probability 0.75 + "type": [ // found these types + "Boolean", + "String", + "Number", + "Undefined" // for convenience, we treat Undefined as its own type + ], + "hasDuplicates": false, // there were no duplicate values + "types": [ + { + "name": "Boolean", + "count": 1, + "probability": 0.25, // probabilities for types are calculated factoring in Undefined + "unique": 1, + "values": [ + true + ] + }, + { + "name": "String", + "count": 1, + "probability": 0.25, + "unique": 1, + "values": [ + "true" + ] + }, + { + "name": "Number", + "count": 1, + "probability": 0.25, + "unique": 1, + "values": [ + 1 + ] + }, + { + "name": "Undefined", + "count": 1, + "probability": 0.25, + "unique": 0 + } + ] + } + ] +} +``` + +A high-level view of the schema tree structure is as follows: + +![](./docs/mongodb-schema_diagram.png) + +## BSON Types + +`mongodb-schema` supports all [BSON types][bson-types]. +Checkout [the tests][tests] for more usage examples. + +## Semantic Types + +As of version 6.1.0, mongodb-schema has a new feature called "Semantic Type Detection". It allows to override the type identification of a value. This enables users to provide specific domain knowledge of their data, while still using +the underlying flexible BSON representation and nested documents and arrays. + +One of the built-in semantic types is GeoJSON, which traditionally would just be detected as "Document" type. With the new option `semanticTypes` enabled, these sub-documents are now considered atomic values with a type "GeoJSON". The original BSON type name is still available under the `bsonType` field. + +To enable this mode, use the `-t` or `--semantic-types` flag at the command line. When using the API, pass an option object as the second parameter with the `semanticTypes` flag set to `true`: + +```javascript +parseSchema(db.collection('data').find(), {semanticTypes: true}, function(err, schema) { + ... +}); +``` + +This mode is disabled by default. + +#### Custom Semantic Types + +It is also possible to provide custom semantic type detector functions. This is useful to provide domain +knowledge, for example to detect trees or graphs, special string encodings of data, etc. + +The detector function is called with `value` and `path` (the full field path in dot notation) +as arguments, and must return a truthy value if the data type applies to this field or value. + +Here is an example to detect email addresses: + +```javascript + +var emailRegex = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/; + +function emailDetector(value, path) { + return emailRegex.test(value); +}; + +parseSchema(db.collection('data').find(), { semanticTypes: { EmailAddress: emailDetector } }, function(err, schema) { + ... +}); +``` + +This returns a schema with the following content (only partially shown): + +``` +{ + "name": "email", + "path": "email", + "count": 100, + "types": [ + { + "name": "EmailAddress", // custom type "EmailAddress" was recognized + "bsonType": "String", // original BSON type available as well + "path": "email", + "count": 100, + "values": [ + "twinbutterfly28@aim.com", + "funkymoney45@comcast.net", + "beauty68@msn.com", + "veryberry8@hotmail.com", + +``` + +As can be seen, the field name "email" was correctly identified as a custom type "EmailAddress". + +## Value Sampling + +As of version 6.1.0, mongodb-schema supports analysing only the structure of the documents, without collection data samples. To enable this mode, use the `--no-values` flag at the command line. When using the API, pass an option object as the second parameter with the `storeValues` flag set to `false`. + +This mode is enabled by default. + +## Schema Statistics + +To compare schemas quantitatively we introduce the following measurable metrics on a schema: + +#### Schema Depth + +The schema depth is defined as the maximum number of nested levels of keys in the schema. It does not matter if the subdocuments are nested directly or as elements of an array. An empty document has a depth of 0, whereas a document with some top-level keys but no nested subdocuments has a depth of 1. + +#### Schema Width + +The schema width is defined as the number of individual keys, added up over all nesting levels of the schema. Array values do not count towards the schema width. + +#### Examples + +```js +{ +} +``` + +| Statistic | Value | +| :----------- | :---: | +| Schema Depth | 0 | +| Schema Width | 0 | + +```js +{ + one: 1; +} +``` + +| Statistic | Value | +| :----------- | :---: | +| Schema Depth | 1 | +| Schema Width | 1 | + +```js +{ + one: [ + "foo", + "bar", + { + two: { + three: 3 + } + }, + "baz" + ], + foo: "bar" +} +``` + +| Statistic | Value | +| :----------- | :---: | +| Schema Depth | 3 | +| Schema Width | 4 | + +```js +{ + a: 1, + b: false, + one: { + c: null, + two: { + three: { + four: 4, + e: "deepest nesting level" + } + } + }, + f: { + g: "not the deepest level" + } +} +``` + +| Statistic | Value | +| :----------- | :---: | +| Schema Depth | 4 | +| Schema Width | 10 | + +```js +// first document +{ + foo: [ + { + bar: [1, 2, 3] + } + ] +}, +// second document +{ + foo: 0 +} +``` + +| Statistic | Value | +| :----------- | :---: | +| Schema Depth | 2 | +| Schema Width | 2 | + +## Testing + +``` +npm test +``` + +## License + +Apache 2.0 + +[bson-types]: http://docs.mongodb.org/manual/reference/bson-types/ +[tests]: https://github.com/mongodb-js/devtools-shared/tree/main/packages/mongodb-schema/test +[npm_img]: https://img.shields.io/npm/v/@mongodb-js/mongodb-schema.svg +[npm_url]: https://www.npmjs.org/package/@mongodb-js/mongodb-schema diff --git a/packages/mongodb-schema/bin/mongodb-schema b/packages/mongodb-schema/bin/mongodb-schema new file mode 100755 index 000000000..c62e74197 --- /dev/null +++ b/packages/mongodb-schema/bin/mongodb-schema @@ -0,0 +1,231 @@ +#!/usr/bin/env node + +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-var-requires */ + +const { SchemaAnalyzer, schemaStats: _schemaStats } = require('../'); +const schemaStats = _schemaStats.default; + +const { MongoClient } = require('mongodb'); +const toNS = require('mongodb-ns'); +const yaml = require('js-yaml'); +const pkg = require('../package.json'); +const Table = require('cli-table'); +const numeral = require('numeral'); +const { EJSON } = require('bson'); +const ProgressBar = require('progress'); + +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); + +const argv = yargs(hideBin(process.argv)) + .strict() + .usage('Usage: $0 [--format= --number=]') + .demand(2) + .option('n', { + alias: 'number', + default: 100, + describe: 'The number of documents to return.', + }) + .option('f', { + alias: 'format', + default: 'json', + describe: 'The output format.', + choices: ['json', 'yaml', 'table'], + }) + .option('o', { + alias: 'output', + type: 'boolean', + describe: 'Print the computed schema to stdout.', + default: true, + }) + .option('s', { + alias: 'stats', + type: 'boolean', + describe: 'print schema statistics to stderr', + }) + .option('p', { + alias: 'promote', + type: 'boolean', + default: true, + describe: 'promote values to Javascript numbers.', + }) + .options('t', { + alias: 'semantic-types', + type: 'boolean', + default: false, + describe: + 'semantic type detection, currently supported are emails and geojson', + }) + .option('values', { + type: 'boolean', + default: true, + describe: 'enables the collection of sample values', + }) + .option('sampling', { + type: 'boolean', + default: true, + describe: 'use random sampling on the collection.', + }) + .describe('version', 'Show version.') + .alias('h', 'help') + .describe('h', 'Show this screen.') + .help('h') + .wrap(100) + .example( + '$0 localhost:27017 mongodb.fanclub --number 1000 --stats ' + '--no-output', + 'analyze 1000 docs from the mongodb.fanclub ' + + 'collection and only show statistics.', + ) + .example( + '$0 localhost:27017 test.foo --format table', + 'analyze 100 docs from the test.foo collection and print ' + + 'the schema in table form.', + ).argv; + +let uri = argv._[0]; +if (!uri.startsWith('mongodb://') && !uri.startsWith('mongodb+srv://')) { + uri = 'mongodb://' + uri; +} +const sampleSize = parseInt(argv.number, 10); + +if (argv.version) { + console.error(pkg.version); + process.exit(1); +} + +function addTableRow(table, field) { + table.push([ + field.path, + field.type, + numeral(field.probability).format('0.000%'), + ]); + + if (field.fields) { + field.fields.map(function (child) { + addTableRow(table, child); + }); + } + + if (field.arrayFields) { + field.arrayFields.map(function (child) { + addTableRow(table, child); + }); + } +} + +function getTable(schema) { + const table = new Table({ + head: ['Path', 'Type', 'Probability'], + colWidths: [50, 30, 20], + }); + schema.fields.map(function (field) { + addTableRow(table, field); + }); + return table; +} + +const bar = new ProgressBar('analyzing [:bar] :percent :etas ', { + total: argv.number, + width: 60, + complete: '=', + incomplete: ' ', + clear: true, +}); + +function sample(collection, size = 1000) { + return collection.aggregate( + [ + { + $sample: { + size, + }, + }, + ], + { + allowDiskUse: true, + }, + ); +} + +const client = new MongoClient(uri); + +(async function main() { + try { + await client.connect(); + } catch (err) { + if (err) { + console.error('Failed to connect to MongoDB: ', err); + process.exit(1); + } + } + + const ns = toNS(argv._[1]); + const db = client.db(ns.database); + let ts; + + const options = { + size: sampleSize, + query: {}, + promoteValues: argv.promote, + }; + + const schemaOptions = { + storeValues: argv.values, + semanticTypes: argv.semanticTypes, + }; + + const analyzer = new SchemaAnalyzer(schemaOptions); + try { + const input = argv.sampling + ? sample(db.collection(ns.collection), sampleSize) + : db + .collection(ns.collection) + .find(options.query, { + promoteValues: options.promoteValues, + }) + .limit(options.size); + + for await (const doc of input) { + bar.tick(); + analyzer.analyzeDoc(doc); + } + } catch (err) { + console.error('error:', err.message); + process.exit(1); + } + + const schema = analyzer.getResult(); + + if (argv.output) { + let output = ''; + if (argv.format === 'yaml') { + output = yaml.dump(schema); + } else if (argv.format === 'table') { + output = getTable(schema).toString(); + } else { + output = EJSON.stringify(schema, null, 2); + } + console.log(output); + } + + if (argv.stats) { + let branchOutput = '['; + const branchingFactors = schemaStats.branch(schema); + if (branchingFactors.length > 20) { + branchOutput += `${branchingFactors.slice(0, 20).join(',')},...] (top 20 shown)`; + } else { + branchOutput += branchingFactors.join(',') + ']'; + } + + console.error('toplevel fields:', schema.fields.length); + console.error('branching factors:', branchOutput); + console.error('schema width: ' + schemaStats.width(schema)); + console.error('schema depth: ' + schemaStats.depth(schema)); + } + + console.dir(analyzer.getSchemaPaths()); + console.dir(analyzer.getSimplifiedSchema(), { depth: null }); + + client.close(); +})(); diff --git a/packages/mongodb-schema/docs/mongodb-schema_diagram.graffle b/packages/mongodb-schema/docs/mongodb-schema_diagram.graffle new file mode 100644 index 0000000000000000000000000000000000000000..4720a792527b2e4110e7e27b8c372a477ddd3c0b GIT binary patch literal 146874 zcmV)PK()UgiwFP!000030POu)x9c{NHV9vT{VS^LmaAWIl1P5*?e4{y6bF$MC-IJy zI7^hoS^V`4Xxq-6&P=C|tLp8}THf|P=WqZK00I$D1OkEo`G2qX+rQ0uKh%%z|NM92 zf8hWAZ+UllT?GGQeHm;VXS6}^~; zbWDGK0w2LE^bf!_faZDk)Fub7gN&sdGR88^wWKvH+bpWwL*Pff}bvd3yYruTZ#$c#&?KW}WvA+N-E5RT9;_wp)1k(goEnmHeBR#_CMjiyI2A|M|Kg1d3qs|KNuk z&mV$5#7n*zDa%D&_#gfGYWrV)1f@-4yFvd7J{@vsFQh%9>(3vDwYg{q96bqsDlXd8 zRTDn9uAfhM??}0&=(_&8?tZ@RzOL(^uVL}wry`+pkkG9a{vq;fb&e0{?IZdwFAC(D zyrO>|*-ys{Q=;CcFXA2jb89zd5!a&Y^QTCKil7d0K2%6m{i`ZESp08ljsB`uczykT z{d3>^rRyp=jqN2dgmU@w zph&A~S3mUy>K+MK!^sVUZ&id_B!3!q`l;>X-Me{m@N*AKdsrm=`oyOe49-aRiz@U< zS?q)%fS>rwxXb!I9K0{qKr=+PHJc3ZUp^^^9h#NDG*nJH0f)r{#r?Uha(;hSryKus zL*$l+0!y}d4~bVTfO1o)d6nhUQh09Dn}Wp|^av8-E`rR87Gpbd!_-qjd)bcjH>P1`5r`Nle~FMBS(D=#_7T$~aH+kcwXhG2qW7>@EjK zzc>14X!+Af^F+9LF*mh@NC;_qH#_-X*yuj}Om;tZ-dM=wrR<@@+c5=|PwD#cDpa^Ivf1rypqx+%KfXdzpn` zfd2C%-6;^PnF0J^#&Z-t%5N(2)I-%^$_RHEQ~3Rp`~yjH*@Z&fs&{89d|%6N8up7= z!A0_%j7Wk;-S=Mz9~i5J(&E>6=N4-oK6<=(vbNFE#7&f z=3k3AmZrcDgGjjMr{||Z?pak-H@m4{j<%U0{f>Fw2>C1v`$+Qdm-k}xA z4KA>_a2k|j86JcX_QeuX!uhP+%IGd+fwH=BDCB{{m1h+~H1)8YDbz8Mo4kZ>K&Z0e z)2r}e(VX&x$x=D@q6urO8Owzea8^6JS6raa+TG0ufnhbgEN%iQb#W;RSX`8J-7Bk9 zRB62|>qs;n`dl_A@p#d(rFF$qa5$DXM~vJ0%P3b#QWop#zK@b| zu@-(yB*(HP7r#pZZ$~A8OJ~}Cxs)VbX&d8dU#jUNGDh58D|PJgzU)b9)Q?Qb9;A2R zxGtAV)Wf-z*e0>8!wCFA7SFD`3YDzU?tK+e?XrDwIZyWCf1bZ8BO0++r5)tFJPE6Q zm)qc!2!<@btkbfZjl5XCzS@#ZT0SgZuE$np1Aphasv=&3ljpq@eF%EbKPnC%%2j|X z;W^BOC{r>#q=mDj)aU5rB?Bz3JlJTcvPhX9vF2R`g$&}_%O{1?V{R=oD0t6_R~xFq zo(Zqnt0zA3yj!m(GMX~u+FezsS&ZuGsof>*s$Nm2B~yffQAsPStLd$Bj|*QdFHO-e z`TA}&3o?a==didIT&@>^Otg!2Wdu6XT0T#!eW3MpzV}#P+vpz-exWPut#|q@sLR@z^$~Bx+M;#x+}uP*)B)dWZ_hqxOZ$5J zDJ*_)+uKnLABW^}Ti-Ets4w^Mooa{va^H)W^8s(Wyw3Xrw>+d%2@FksJnAMe-18F_ zKZ>{BNbn~vK18DlUa9!XjDhX>dLRw5?Yq z8(2)avA)~eYJhi*f3`h~@cl@$FFFByXytY~**DqKe!J9J%F%qcxeJ{8m3!0|`YpA} zG0=C#c4v=8ySJMse(d!kUyksm#$5T{?Fjdq_;_ zyk&6qvD(27yp9X4Yb5**m!EF*6na^k+(r;DtwA7YG+(7-YjZ9ORUr6buJ)@;1R}Q2#YVbvlHId=oMyEhhNH4SgC zdo+ic2Q7sN3omx~Mxrvdlk0&X==gGjX4-Cud!mpCu8JAE6e1x$8_yD`igQj66&D>2 z=jU3psx6&&SEM5MB!rv?uiZxhmn4VVjy!Y8^Kjp*7cSY^-99QodPp9OdXv)R`DCM~ zo<6x3M~)6Gp6;(bAlEoExKUJzGRtn0eBbri;LX~dqccuf3`$flM`1m7YIrGx_lYv@ zmqkI)D2|WxRq*hO=3CcOMO1NIU7H1BI`L^3pPc?sqjG&E( z_?$hkdi)lOqr$l{Srw&%ojXP>-ZrQ-a4Y4Uf9bMOMw76e^`(3oBB$GN$-EGz)y^lr zIvy2|j{2&EEq-KiwM|+_mvXJ%_3hDab#gy|$Fr@6mucx!RD-v5rZ)pADPOhkIt7}0 zdfXl?+3fbN@D!%jPCaL*KH5q>ErTxU_JjDi5GSsVq(-%NGWVS0)2y>IJxquq?+-NLZ~|Za^ei`@C?z z99;b48qCVuO4N05H20E&w?yf))$U>Och(t4>qWh{%p8Y>*v#0rFC?)w*>N0LYGWGr zcop}!zj^bh1ANeY&vsNo?vV50&4oweszu>7I}^kf({|O8$0o__ZY8@c#kh&5`1n$f zxJxBjuG#yVdUr*S@gyuxn^ff;HH*GEHHp~`jm_-^^V!|mze{-hz{eq{Kw9< z>)q}yKVN+OE{8iTo|NCaJ1F^HM)Ncx)OF8&{e1PtHkuJRGWBj6Olh@Z6+(!^do+E*jw6C=5^~aN8uM?v zsd()3er0C|Ii0v7qcEr2e{~n^qDB6>y9D0-+wS^HU&$Y~DP?H9xst$NzPigjP@mUV zSr$$*1i%L&aIe+Hvuf)h>xyi6k4zK9lZCWQF!SNLGd;07xAsxKvEHZ4?yv)&5NVP2 zR_-8tki8($PV&tN2nZZ*S7^gNk(No+@D8oyXWH5LHBP8pyY0)zQLfIZR_k}8eIA2b zlE(MLJ$<~6?K4fgmvMg${Smc&%u;GH@B3wNux#(Fura7L#_D(v#Ef@~qn* zyBF%NU;2t0zMM|bxQCVv{FTtJ;98J%0#$G!>$qoKKvqA5C5xcH17Z8mEXFC#ecNO_iN zCBS_#^q=GYXWRdqv>o!a_D_ZMpX2^#+y7PC4jJ0~g`xi(_dnbI-=OV~r=8y=r2icE zKimGV(00hs{$DZlpX2^#+yBMe4tYBM-9q}$asRXJ|6gxAWa#W~82Zm~|FiA?!fl5< zUHp)c{&U>_RojjQD0G3G9rDpTT)Qnkov_9J`H-pF^tps}BeIDtrPt-@O!qZ8C9_?H z)!1I6l9v;s`UHXR$~@(GOrRLATdp8AS-V+n_AtwV+OKO>jP}dlg-mKuti5JqMQ@!h ztYe0j^NeBH^*b5RMq03S%*r#%53jkJ0UqWd1$gUHkg`*8tPA7{Gr+Pvdi?~l*EA7-(qhAHP+rOkL~vcFgvdx*P?N-EqskVtpK$30M;e~ zvhb5c5E9I}3ROzu_5MHy)Wu4x$3ZGvCJ4GwRMh>9cdQ9XkesG)Gg#bNefmg51TlM@ z>z*QxO16U|7*FQ4mrJbTq$N*z8~fWKn9;5chvt1S>RSxI2dc-3!ZE7y!<|Q8u|>6* z(R`E38;iEWX|nEGVg#?@i5Euu8AUp&Z?*f%s?vpl9VCVe_Zm$_lEZTGzDBQxTnec{jU)VIZ9q7RPt zO0g6%ZM|W5%mxm5E=ccs#H@HN*dsa3`O(y)aDI(d7N5x5l->7tcC6bW^|HjCIymF5QBQnQgyWFwJWSvmp1ndK+P8J>mD!(%jOCTDZ%NQUPmXbpRBDTk%#UEzrb?BOCg*lKuCaN>1sWXV`*%86}Wd-J8<{dJ$@ zMe@vQrIIddl{jWX=#l4=A>#S;BJ*X*VFqt=w{52C8)vw8)?h?egq^&wV=ASf%ZDeY1qlp<4j!Sg(q58w~veB~cpBQ^0)UiqzS z6afEsUJ(FrLGk`mMgDhj``jIMu8+l1(Js-gA)0Y}FW&?E3IDquo>=GA5j&B!JLqTq z9ZDys5gX^wv%tZ$vTGQ?8+M8rNC)aD5V`=h0ZuRYZI&qh&MWA-^AGN$(1kx(6qQ49 z)PJxjDu;;wV?|YmoRE?tpNL)sU#o2zz$iB#j$`9j94f{R#$vF<+Qx^#2k#D_X+ zq60Ol^fXl<)v+DDD;bHl<{mmJZ<9P)ap_azMt)_HCFnoXWD|TQsF^I zTg-&;=3Hbs#7@OZgM_QV`99h{<^aahR(mX}Ym#ktkt_D~o)-&v4W&x25$jQCdtHBA z1hH4uY-l_~ieGV4aMQL*tnbJ-iZu#S1!9FAy80;0#sp-GPPK)4=DE$l;*?-uxmUJA z1o6gtkC`#kmIa)(;p(!XTPT3#(;a&{6{=5{eMLW*<%~PN-(3;cU7v|8sDU=^1&O0Q zgF5g|2uQmiyK4c`7E^1?o@K4nfVy zpEMz8Tj8=xW~<<5#C6P?(K|rvG*FC<(_pGz$=l~=>raBe1de3otU@uZ_Trak}+?_K@mdnGtZG$qz&(~!U zdiI7uX%(^xXp|=f1khYB5OlrP^;`JoQb~qNq3sbN%Wu6qHIhDE0#r}GU$bqEL#)soF>@wqYz~MxG+9n+8 zsMe+0i-q9fP15DO#XAQR3-UPEJsSZ`jJc6D&1L~;+r?g>S=M3s+F%IE=PV+n@}=u? z`>8{Nn$jC1&m?Q--IwRtY|BLF9f7;BO2Ntq7TOjj+uN(CI-1yb2R1d2?;NXGw+HF_ z9jk-h-TBPVgs^FE>SA}B5->Gmi9Ul6H5-HGO5ZUD39FM-tTRnaQl9Hs7Gx%rF}pLb z(zk*0Jo%;veAd;5f*qrG${<d82XeF*zC~zj5glbquZQl6YmF` zR8uEE6Pfk!=(1DsHyZcL96vQC(3T2wYzi#?Ia?IE!%2U=vM1(V`27ov(0F*8dYfZW z@_6*Ie-6D_N3n%VKh?4oCTy$OymnUzY~3DUd4MyA=^f)0^}HTlbDhy0W)^(7*xuB? zXeq)2H13Qd**n?Fal6*_Hf<-b^2adLLx}Ckrl>sHg7zcJ3#LQ+g(^W)ooMWPL$XsZKlS@{OR__D|6WqX;qIqLXF!!mX3-Nz&( zWRNh6ZK~$^b#SaujLGb}@;bkjGQW@M4)EqNY6_|2s@a26fJNJFfOf4|9f(~mGQ%}y z{q)u=!0LTB4qw%zo4GHI^Zly7^_xyrr_Q?zz}mRy2@M=5yPmZR;ohYbw)XTuyd3(u z*REtA5lr$<>zDPiG46K1O%Ix0E|-RJ%50Fb5$zG{_fL>s8fXhzdb{OYguoP#=R%+v8*iJyEZr|*YdVpq$blBo$f0rZQ%Sas0FEBkSv5l zgIoaeaoV8aupl{qjnqf1&Xf&wVl852%{}+@wj3)^rdK*w1=jo&9}mW$&%QxnjU?tE0%8b&+f}I(gi~P)^t9QQ($> zRnV>tN1W?o9kCV8cR5~W+QM^|V4`eq`c+0o9m(E^tFPGJ*qyy)HMVi|qBAWlpWXL4 zpYl%0a8HVUq7HvVCU=*(yXIdnlP&r5mTJ%Yh#BUzK{=F?Q+CzbhV08cMgx~niFoa0 zL$nOH@MaB39Ut=eQStW-eSo-vk!|ikC+s=bY`3Tw#3+*!U&Bu9q~r>RK3w2rjF`o%0xh#Z;tkoZ!hy`CugEE z?(rs@n|IRX)H~5<{o06A#h$^afzc_(3uIBDTS%lL6M~g66IAw7K0XeY{4mSv5S4>- z7+WkliHB_t2YzrLdV5_;^7T05eBpG=iCSA^!^+OOf$YTkh1hd1Xj)%$xhNUdeh}q2 znxiv%aX5y-Vb}O7S+}||FwMog1uoKYRWLE(rY!_!P>TYRJ;-ECJ>I}@#vEpg=Za#z zes6k^s?t}PR3sWbmp8@=x)29ti{m$N9q=x9ZECq++)O$sxKme~?t`XX9 zV|zj~%#BIA!q8>4MP2WMepMWIPzLP~_ZX|a3|_%3wk;7%nC3;SC0Q@T&t{*t?;3~9 zq9aiV$0_a@sH|Td6TuoH#suZ_IL&U%-k)#CX;0POPxlDBLB!@_AGeRsal6dJg7ZC$ z8lAGhcB>1#!pyjoR_YZ4A?Row$fiF__h$?h$ElFiN5t)rhQ-6q!a5S)NbL0*F2wtF zH*C^^Kz8HXX%gNRW~WaKzp1wr$LDs!R4y7EiMSQfbv>Uh zo_n)fSvcZCeHo43dQX-Br*`C!9>q#vSRuqkyLIl^@p{C}+sJxFd3(xxifp{-(!|8> zahs>hn`9SFzut2FZW-MsbR<8e+yrIdXW;li1xXKhwJd5W0g_2)`@W!#6U7EB)WVRI z#k*x!Nyp->gmS^_`uP2XU5kVjGndc)1Z0L@3iTM%R|@gJ8aJ1S>mQ)%LjA3^(V=F1 zCwn)>Luy}8hqA=2LA!NcC4u$RJr`yns$&T79$f`|&Y5sQ+#>ENDf;7lKB}IZd$2h? zkf0*NctD=cNKiK`>j4D-*a!hL?Hhe3a&A@|-1Ka18c%mH#Qw60M>RdZE6Uqm-K{yF zl(%|CJiiw9)dS0EW-qy>V#j#BL(-Ru=mO5eWbUbDkRnTcS!-Kuhg~+*c5~J!`N|__ zqw!MKX^R`kkel|31~3#kRN8Y{&}1<%xn9&Z>t%pf?&%%}F?YnT*Hg;X7Xu}%I9%R5 zbM7v-J=c_cdmj`bICD>sD6`Y_v<VyS(I>}C*8BGHbhKhQ z2)AorTY0Rgx~jjI8!rUHQ1aZ7i0F*Y0IO724azUTYnOP1W&lc?knQsA9Szqd$TW_Y z{6Nho3mc2Rh<9dWPt=-zX4;kNA-tb6i9SNRYhYei)bsa7A#&3kD@rPz8Wat9-swFY zO$Y`5na>NEz(49H`*w|XVgI971OV`YV*l58je7Rt)D8ll?o8gH{ZNCZfX1S%#n;Pd z%eb~=PY$H+K7L8${e6C~XrEMWSrd+>tIn(bcArV7XNiK>059Gg%z%HxQS6|t_w=#= z-j;fa>t^ss3bp%^v<{SOQWO5_(J%LlX9zpu%eS$IdV>LbHD2L_6;a*;0^X(x$o0Q^ z^mRYphp<13JCq03ad5~SVAYLHAvGnJeQH12LBam*BX~c=ZlS_!yBDP5?^1?V2mS!j zKU0jXi9bN}&lDqM^#_RlnPOP{j}ZOyeYC9e2Z;XpK5}vY0iypN+x!60f5SFEM)Y5^ z%?}g(oo)UA(LYm+ivIzkf2J4_{|AWvnPN0>e}L$pDMq#b0iu7V7@0zUfasqoMu_?k z5dAa72vPq5qJO3gd5Ql3(LYm$tcl-EwDT%}U_#Rr!*PVC0{l*+e#b*Z!P4D9Kk-dghTLTt0V`svcr!G%v~ji-BNA}G*` zX$1)84L|b`dKNYSHss*LzYnce7NFk|1;A-!4!{oC8~A^^iu@hdXiMoIUC)BopIlyl zd`S#hk-Hl-hCO;#VE6C~rYIYj?_aD){2I#37bfo+ub=F z9Q&I5aNGa>D>MK?UOy{C1EN|P@KUom_&ZzBY&ofrCY?$rn@={rZ#P8>y57Yi}u0`vxe-BQ6 z+fwsiy_g8>coD$+3k9Cb^a}PtL_qNt?_L=1U;#3^0tZ`poB{Y_eDMzc`g{%5{(CBP zx|n;-+`~D|TZ{dNgX8SlLWmGk@8IJh5CCfp+WDhcNPmlA!llBwH@p8LD z0TIG!h(eWsOV8r*maAsBN>EUgZ(GO?$+~CG>pHC0`{{%`}51pCFrj5kl?OLbvlYYj*QKt;TG@X^ibj*AD!* zc~w{e$elkP2rWGH;FO&V*%P~bx4fv+T`2r`?t#41c zJbJ9VKU}=nSfdNK3PvNI?7GdYrx4KRL^~5$jFvKiVE5c8(f4B1_K*s>kD$H!%5UV@ z(|&gHvTa=t=hf?TanIuKKsKsYvB(?QOjvwq#4P7|;8>t5$_ zA)WDkn&bG})2WHs)5?29<^YtB?GWj~DQnjo>>5QEj;@6D&B@O2Y!%MW*S(Cn`4x61 z_s+@NDLu@Oz5FURt<)+|QqsH$xbB_$`RAmu95>~Kx*V-}P_O93ltJ`%Z&x4*EGHfM~ zml$tm&YI4UukY&aaE_ysz3yM{wruv{DO;t7xW?9&cA0p52lH{g`cAK8i{*)56SRFe0*MJo(@b${e)>9NU^`2 zR(>6oi-v6;w)rU-qhh=QC8N(NL$?UeF?3Pgh$a|aFmN~dMt-vAJ)fE)x^!ZRr{I(% zDo&H158wLf;`_b#@w)t5cG_q_?M5k~ATLaZGLl zDQEjC*;~mWyUYsYDN>J_<*Y}Cg|sws0s*i0zqN-hw{B<1hT8B+_hQYaw$ve6|5@EdEDEsz4xza>Q>xS^`4zpp+Dqt0U zG}r^+L3Bktf5)_OV%_FAWLEhyB7cjEq&)`&(5V)LPzmI^g_&OeQ z3UW7>F-?P100|(>rDAQ{Z}DvnpTs7;?p<~u$9a^AsefhHXP8{OJJPUkOEI-nBbh-u zXgd|4xCoz{GVX3JURntLIB2~i<+3!npT6_KWe%)$0x2qd3 zwDcBp>J82zS_Fdyh8xf%2);%>Hw*Fw$hwe*t`c$N@^an%yu02^K0D^S>6$j}xT|ly z?jICMlMGBUtl{)j!R`)C=cS$sa)R16aaM~(#t5eNJ{?nCQ@FES+}t$|!tpXElppo) zdYjH!O`Sq_jl$U~GiYf_yq3CB%pHj@w-3DogNvySNdT2wS$A$63@={OL>}vufbecf z9XcU|C)Cl~b=+0AW_UXHszy%cKsA&oP)aORaejOQAgBx-07awEQM@KTeZ!{&9{X0y6e6AuRn!A#dcHseA# zj{7Se%{{XsriG}5*q-AiJ=yoR$*X$Y<(6k1gm)!rS8|KDH3tHqsgDw;xJR+ZbT!W9 zPD4E(&lN+L)|<@cv&xp(<`{=_LA94)H)gfz+y>2;noB52=>ncl$YelaxcWJh{byZ9 zU^9s23e8yvJf>qH`F?c2)uYNDqp``~6YiZv6~Eugh|$Bl+_pgMB>lPjr21T92+>Opu|nKgndx~&pr*~`9#d4i<9V31v-txKKBI0Hq{pbCy{sBSJ5#PbXrZp z6~%|kS`?=21zHgy5TcQEsuKf|f2xH$4#gf!=h3-EiKxGCID4+o4v?y* z8^dhPSZHk%7vusu=MeOg(0+(^g|N(=ec9vpJA%Y!dR7`-Rh7F<>fo>p4?~8=l8Uoz zh_Q(C*+qR|;#|Xl3^B^4_iRzfCM{D)%dFja%^~y&pX0$LFCzrZV?jA&O-}Sd8T-4n z=_RR%Rq9+8RaES2!zzI97~lWsI7c{1%gA2mdSmr5V{KCHsinQy)ic~K=@lY|d=EiR z>dhB-9p$nL>Gp0~^9`+lTd{c7HSR$CkjPA;34ClBT_z;ZQ#YtN;XK*_6sBQ74%=BJ zE5TRd?FGPdoeTgU!}}j^*dpZY{3*WYQ{ceY=z|Y<=gT3+8`w35_x$Dpng7xMcUDqS z@gn;)d>rp_G^V@U-4{=;QE_6uP;$Mx%c@%g+f453_1GN_JpXFwC-DhYQh=1da(#Ug z)N6;o$=l=%dAH`*E2Z6xh@C|1c4eFM^4h8X8naEzBovc7YY9*&az&5~Yo%p7SLPC) z{b7hJ%)W>B{4it;HyBBB)3LoPH#!zx$>_`jx8QncKo5xc9EzS>4T0X!FC@1mX>xfG zRDXZC>dD|_KS@=xJij~ihk@g48BlnPc8wqI{sPV9vZ3N* zP06LxG2ZQ&87>Wh>J+ZK`oi3d@<_2o7$cXL32`DP5e7jtUN)u6%otwsFl(w!jnoLG zDa5a$8s7A`D2fuq&qe0Nv&>kBeLS`lKr$A3Acs zeHW)^Xq#&^0^~5u}Rpr!|>i_ z3v-ULQ%om01kn!H9%TB)p7se>Ra0S`QpT^>k?F_xvM!=@pOJrl|5uphMNoNkt+M$_ z;ZS}F+8b{hK@M_8{Q~kf@N&pgD*5j4rlRxjsgjJvam%Kxk)Yn;fQ%f{9Tm)^))lh~=BPE>>i0JQ`KaXjU^m#(iA#-R zx}Oib^EpZChh}&YvLdswjLq(1Kg?>*02z#;v*;k@L0W?(X3hOAr;4(3Iq~>{Kw?WKvYZDaEqQ(e;)$Tnv`?`7J(!oJ!16LG)!`rF1+rE2rL z7w)Pqq!%^pkqParO(1gA9%Z~LO z@?u*1!YOZzM&=0G(}{xpOViICy&Vp_(-e4beie6X$2&?|T{~f?Yrb7;C`4=M9%oFhBmV}g=0+t^q2wQe~jE+Ny;sORbY2rXmvZqv1BzSi+vjAgS9 z$~tKG>T)?#yR{j^7fv{ShMjVCo#^a-BD2I85ZBeyndDUdKz5{!d_n|Ao9wBOH|{ZW zH;2Ar>h&@Ki6H2jwIBkxamn=dmuSEdqK=ru$+;kpKr9%EDl^X^ zA2S(^0fC$wC9Cb5)QjJKoKa{=D`c|RtSu~SOFH$_p*@4^)XSNNnpbG62~q{q=c?f5 zjy@FwlZJ=1r(O2EM?s+lg*|lG&$LC%Lh(Kfw~c*J5u1BW*AYE4c9U*K3UlALk!n~; zLg``1Gi_IXEa;VGuenYass;p~;cm>DWzXFu8Zj|@W8MX#zstUOwGXv8+wps2gSnAd z0Y&{&F1JODP%eR*8hYEMH{xX@Kqlk_9uJrNqRT$)Gl*(_(m_AaEyx zbDKpmJt8p|d;bE`)4fY?!;O7DnycrH-0r+h+NMNFbXdj$H*r3=dW7#IRH;GOyC1I$ z8BQXf9Ck|M%A7Er%P=Xsa(V=lnL@4g6ri@!lSp7ba%QP8zC)o(C64yO7$=~b^s`WB z?s4K~E@U-K;ljczqd0`3P=5$t{9s&kG-5=XX zA%rC6I3bG_974+X>D9b7a1LvcqQfWhkVDnMpk)Jy=>wp3w7L)3#336KM2-@#0(MXy z({S29$j9ksv>bROQ@Xv~UPrg;(;ZkW9J)GG7c87%#;@0NIZYV=)ya zq2y_HrU-mqkK+$^CqjQ^NGh_7+|YX$)On;#lT|4Vn3#$J?q2yRAMtnJrIS>~gx57L^!itqyAmV^3Br8LtuHdBJ^X zJP)n2t48yM;(?}M)k1XEq{Z>{NY(XvV(t539{G}Ln`C!iZPL14AJ`4?{C(`B=J4Y2 z8w0f^roYY0`?-5+*si-`x8kN?Z=aRmbWyNH%c15kyB9ap2AdU#ZJGrL*O(ml&+FSv z$N`VcnrmB5ZRu055@=BgBSlADVs*(=WnG)1Q;$RehXHahw5{k8 z**2=b?C742)0_Q@`geR$`oq;fqB!dNtr^DTK0a!uZHJ|o)Q#m! z%gxPpCN2t2U78DW`y9P)y!T&sn(}wrPc`U;FiH*(&S@lLv z-$&i7kEfhkcSAaf`)u;2K+`vJi&f)|982Jh@6<3)~;!>D_4?&~AX-ZmB7CXJkqud(a| z?V`@h1T`Qg=g&g(L5YkJEh$7yT9qPK#{Zh5q8`^PbXq~@b18*ps z)Nd{4X2UNpUn;D_Y~lv*6gvfoXwO&X7AxT&v}L^MQj&n z1)RZjhjfZE>0=a6jNLIs;hy1}sWN*d>tLk%C_;nP!hZ1Ic2n57+RaKTJ>lm!j zf#xu@rRW62nJMC>gM?o)ku6(R$BaT6sh9c?Pn1hp43Y;5BEF7}e9OlzP(6 zwVXfPF&&E3xS?x`e03Y`R3q(-jN=#B-KWGTn;U#LqZsn?l8?Dtj8JF!q0@XK^Pvf| zv9@gzZ*f_Q^HDxLzXeDRLXyXuuRMzM73=R>_CJKd;GQ;OAqWC0JBJsTFP zv&65M37j>=nPp>D+d%LjzF_K^dC?8w2cv5`_sc@E*^$*(bzog1P{d&d)j)X7|*nl-0wPWDi z*SJ}u1B7W~M#|@Oa70j_Qs0N*Ov?Q6+MoS8uw+k}-C<+g>y9d`C={RD)DH#Ob0n$l{Y*e0I_8+2l@CdFn6)gaPvf~z{RCLURR zI51b@*0j7aJb$roArd~!T-_~K)|jg+jbpmjbX-@^6WP^p3cvR#ScqV$s*L4vgK+q| zRtM>#8gZuAx8N3JEfh?F6UKtAIg*IaJxN-RFpwM^u#cn&R^q4yH(_P*WPAPD8<;+~e*| zn8zKnypMZ^e*rz z9Xr1wD=p<45fZT14fDhe9CEyr(SA74%w1vW{7~*YO<2PBarDyamEZTiz`clwsRsH& zQ`^Z@C!c(;e9uO6*&u9VUsaGufHtBCEC)=nxnhO0MvJSE^Ew>cO_Hr?7Z1zdmQ`kT zHQq&wjQIAPAw+Gw9kMF;juxl3rNqqTJ*} zf2x6wj1jSuhHW6d5dzCgB}(x!0{4B3E0rhg^iW+$umxeW9#+^*nxHkmC}8 zQsQu+VPiB0V%qx(Yj^$o*e)8&!ba7(eM#{H8bmfl&v4p0P%1{B%ty~c zp8;=o6jT^F<8~^!fWo(A>iXrYJn2~@XsQ|$sw;8OGJae=%)D1n#;rE}zyyNPvfP{{ zTqg|Ch^W)9nf1)YvI|;&e|k%voXaKXs}aNb#?EHO0qc{si%~2@4Vo#jBT~$S6MIT} zyE4mA!Vo!up!TLh75kFaZR zowaakpN3xDTsW2g-v1{XKmMmKy#Oq4UPX&%Cz9A)Jq?k+{l9ud=Q;4=V{^GWIiBR# ze|o?LYSWXK37}R2u=t-n!t4LsN6_ASAARi@e+2v1d^Pq4Km1Yq_z6_i+@NXr`_Z1- zpar8fJ&p0(!e(ujy;WIQN#*NHqUMD@)G~`|i3xvwtE2BD=p-2EG&+XQ>XXujw|eTsV?&mh6=y9P9>ReDOx(S7ng76@zo1B539^Nw~&^+!2Z z8dcp+w`LP>Yqp67`BXMujA{7UEm*s_3ek8=J6KE>`Lar=%1L=PKWZJ<>XzLq){H?e z@)?M`q+Nt~WY*SJ&DTDRhk&T~JuF_Ijw$9GP)5)LAt>LyLUp%dQTq3YGn{Pi z^By+m6dt8~k!snACQR=}t~+}PPJP3vAhqcoB3e6jyO=YvuDrywJ^D8Zu!ts5batRp z+R!S466o-G(~pilwpdUZe{UsVbL7`$P421O;d356-eKCyTk``)Oz@a(!-3)`I++ zDug0WI$KU+1wX;%<(V>+R9#|7Z=DPVY>ZSYUDWHY_Re^X4UrWdA4om*C8mZd%x-hsVCFCdr|jl`%4tYhCkN%t)J{$;sRSXMPH4Z z^!fz1Fk*}o06vFCg?Fdwk=SIq(4yU2%X^H-l&k(R>4a0OusU8~XMMKo6GYWBY4WF*^&@^5VLVbY13 zrBx>zf$fc{p@hT=#KTw!gBux*FOFAu1T*xnJlU)b zvODchCL*^1{WBBC-~a)U&lk0x-1{`2{l%EP86L8Xr{{Yo2b{>$pdC}3=?^*qCP=q4 zePKiBfwv<3N6tL|Jt|rLPOG0+sZ7~aC6g_y`}zVtu#U~d%aEcou@FD5g{;=gM{FR6 z$9pn;FhShQA7wW&Da?d~Cdw*&D8yG}x7yKk=xpn9Hi6m;P#yAE6zpt`Qr2l^rWes4 z75yn(uRn;z_^(vh!fTFv|5bjfacF4QnfbE4XL%dQeQ{Ju81M#UWwVy?V zfs$CRbX<|4oHp9Q=j?1mM=3;DGiv1TW~n&o+K0Upi50Lf$Jp&(G)#nUTzr`;gEYwr zGf7-%)8(6xGI6J~$Ojx)Xp+=<8U76R0=-3fk~?I|P_~uzzbEzHEQ@ya`7W-SL3eM3 z?gh&M!a|hz8xZ25uP_fNYd;>p{~_HWJ!8brIM2C9iEfvuOnBU zH&`#!wr6Ry*lhoWwC86hs3`l}nGHAp*uE%O5P{7_c7Xi86eE^0vaQaZ6bv z6Sz+UZbPGd@UfYg)=+X*yM}{JbeP!~dmN{U81wJ8%~UmjJ+t>hwD?#?M7}ppDmJ#~+yn)%7es`^NQq<<8@5$;5m{ zv}$xn-DkV>!qHS%@p0_=I&WN{Cto#mcm{>7ckJz~Ac-1! zd&Ek7uSXC+rJ;aG>?iPy*b=#or_8HQN~4thYRF5sI=)Pz{T*$zSF1r@&RV?$v0N~B z8fb+ml-`6UlHkdmGD28-XqW=O8>Q7*n{!{zaqf9rsEp1-r+9O^Q=}oTq3iM4L*goP zqdaZXKzA7AYQ@BEy5JAF_klcQY*#l7HBZD;f8kjTRmF4! zYew8Ec;_&as+^*EVj`c1(0`0fxb2w-@lL}=X*(5U1j>Ht+#$W#LL7GWO}3TGhNHhF zwW+9k;lohysSI!El;)eD+=#~nxw4r&_nLOL+Vo(EF>1pzNn}UihWMd91<<*UXdljb zm&iCquyLgj79ea@XS~7imf`c+xo3%V9-U6<|J=WgxWOf!)9>}gp!%p{l#Y^k!UMtU zg=JgfKIq0&czs;YVasC&a?H^eTz29%5|l;ZczMGg=>ZWd=NxSH$aw#a+rBW854S|4 zYuH$Ci&0%0r=R@6e&X_DR8;mC-qEL&y*ho!`9#MS zgrD&*tHSoxrG*xuJ$ozujJ^G~4DmHYw0LeZ$M)HBXVm!_b!v)Xg^4)gyhPu@*7!=K z^Kq$VkC&E|3;FL;1opCBv zfdT02&tLiOOqEeW%w0FQ{(%$naV4sks7)r+`fA6wSQ2e2;^$zQ&He(1ui-n?8CYh6 zUmR1-JTY_rDfDk2jBy0GKQ(s(tcWJv55ra3#iECae!H`TkOb6&x|kf7mHUXO<^sIy z@{?HQm3m)-{l`D|OT=Y+rrk$aT3f=Nt3f>D>6SsBybGm&^v5JN$;b?IYz8kLB1 zWL<1TP^gYk&9QR@7T%;pS2Ha)vc6GKmABV-YUk0)OFS%sKnxo9<--{Wub)&=0i6M8 z93Rx2mONs5^!K)#(t<}nSLoE5wJLHj8=^}johVL-tn{khLXr{SD^w$z?r=u%W;9Xh z-!~ZS|6&f)%d$_hgxGAo^>s-tb5;<$gl#>k>j^W^-7#6qK%7-c{Vj;Y9{-c)z{G0~ zJGLb8FHJDUSpsk6R?dm;q=0cCbc1LZX*Jh!rDXlt$2yj9KsU$G@Ck*abs~mv3zl}(^K_~y{DiWUEtfx z5Rc2HNbo;t?KvBAPXgTj_}1Qu$ofSPvMk_?&-w!9_HDX0hb8ARQ18UEj&Ag8#YVa+ ziF`UcA}ojK#1aptS(AFTQIH)?yJ5vCYM2AhZ|+kL#uh9dj(LkukeDj^6Ln&%?+X_n zu%I{N%_Scf{!OIMZW59E-sCN}=iBadXk24qj^QN@3fn4@PFz0`D5OU$9S|fcHRt(2 z@qedr%^x4_B!ygOAK0UVj-_gk8Lq@9{Ei{68%)tNVbvp4<1yx+I8&GDs|B#rfKBSc!pn?% z$Gy6ss2=7YxXP-JQ1xB+bq2! zo+U+imfm)H*_Q3v!Qz!wGnecRC8iYlsG z?dnAmFW#*(cWU_EJ0btIV0KJdR_4`gU`mu*$=Rd@QIm&s9^6l2pYg}l@KzHu4hyr{ zBoDkzW@wWUxxgD#VxW)G`mq;z$3!O?xlS>@tDF!2Y+~@>sX8(1Uy{Jf8s+IpIg*b{ z7-cMng@FBlaGrS~uZf3aPJ>*r_kDM(@M!BI@}K;+#tv6M3j*2eYz?M(&`N3p@u`O* z)dX<+W5Sx^)3*@e6NnAT(Uy6Y6k({k$R;F`sP$Qxoct3f4eW{X z^05TBaZ`2F4Mq*AZ}PZ!jacw3`Usvqaa)V+r*LN)+B{A~=a=z@3rYD3T=|;|I1%ox zbDS%R)pgDA@ys83Ggt7xqw|jhy$0Qc^s5oEn}_cvSy$t%|7hf7F+1vC5a*b##yuC1 z6hCJY2~QYXFUcM4xQer?Uuh|Ja?ac;uXiNs>T7!<&)H693gK48M-`8|U~1@T$7VK_tlC|*wPhw9)en)MJ2SE zG`Vin`(|uBa$#_3wkdpRrg@&wTkRzCs?!MSI@`*i-*0YC`kYbl<{<#p3_ROyxWbBc zHwu2_crFfYF)D$~e0a0+j=S@yP{bhutx3Qw(oT*BVCAPa$4A24xt{j^!l%bHM?bos zEGjbQ_@T_^j2cTswL=}ji$g)7U@luS+{hA1;Sjw0Jdb1~w?#SO&~@s*jd`f(Il9E; zk#DY#6hwH-vLF5oE{H4m%A>_HaGS>DZ-gIi&zfozr4<(^n*ucEYo2+C)()eAjAfK8 z&SIKq_}Va6>(1JLb^GLd{4C#i`rFlR>hG4Su&DFxX&4XDZs-`H<>w5#YY;W$RqDvm z{<2)c*(-;+8D#~zn)s(Ko#zNK>6FQVy$?lq#Zu|T$dgjrGzL?1GDB`-IG4DE zVWB_$3^?VKn@mJ8tG;#ac3F;Mx4njm&2V|vuweaJ?BrW!92ao-VTES*po+NDGSxCt z<94}-<+wF&0(+(5_LLpVT9n#TYc39AaesH`uVEozhlvkm1P@-~Yp?z`E{=GnEoCbU z+bDw~t0*P&g! zPb^TdctWwZSeflyZJCju3VeC&6a~s)_gIK&!kdl%pIZEl-K6Fr$?}qBi|6jXF4J65i|zG>VKd; zVp%JN`txlqfARi&rFC`)xCZ;%u;z3fCG*{>&tbK!w13Vpp=Fp!UM*W!Q46IVI!{X6 zwLLK_6i{6{XI%fZYfQaGYnwNh(&_MKBCn6QVMSgdV#ML)7jn(BDJJ`g@)4=VXNjAj zB?BF7yT)g-uBS*$Dt@L19{q+Kg{-~di1(_Dgok(BU=o=Q+lWKM#_o|>mI_fHp3Y~f z;(2N9xkwT!wg47E)gT0YtSR#lkjvEqprvcFV<8zJGjv8LQh3^Gs;k zoN(k2LrK8CrGT$cK9jx;mxKb+VfL;@6K?ew$B7+%$BP}yEEIo?5$!{OOYTtrbMMeW zC@tcDt+y8p$#hR!tr2zGww0&3DUJL6S9lrAp=f=`_o6;QS6SGMX((>R5 zH!}Pb?#*tCmo{}`IBhf`ONn(Mpl-NWd(~^dPP6U)XYd>5)f4_4Ag-m{rz#6K;xWwR zeC1ob_-YScF7HV55mm~p+)T_=F5{Ase}oP2=L(7`ojIA@KX?2@9K{&$S{?ng+=cPc z_!#V9hHV$J7Mjgru3hE7f5|W?d}Om7O*ra5tA4$GQS5_aYs{#(r1;um@+*0`3%p4P zve4v5a-$cX9EQc8X84PpqfKP`R*u}FJbisSQbThuOA~k1(C70ynmqB~Yq-W(T{ltX zT}(a#I+l4@+l4v-Plc5(LHIt8LD`P4oHGtXE#o}$ZbP}@MO~S0GieSc;~XoFJ&Och zY2rOC^U4^c`NBT#VwmCfdfwhjkHmJy9X{Phfwd)SyS~^dM;K`^Swt#PU}?(}VZ)8a z2Q!?-e7y*1<`l9K4^8(nd!mo4Z@#WFE2@R$6g9S@s8b62>*kZf`L8p`@}q1Fhms|N zhF8rLO8;<4Q%7K3@kCu#aXFWE?cN0~CfWtEnCzX9L*Td6OAu^tw^^6rO_T;bSm_O9 z3HnA$rg(r&bFFc=%u7U{#y_Vt%c7jNjC9Pp1J=f#l>2Mh?hME5$rJ>Dg1*6}(ckdk zEVo24Yaz+S6wJ}sQUdUtyyZp3HlNO%@Z5?iuqZ#XCswDb%VClBf?6)IjHUPSuk-+5 zx2)Qv3yPMf2x1|J)A*)s23CZy|B@Fa(-p_Dx^*1SHq7yP^r=PkcZ0mf79AJL%UWWF z8?`6JDP|DONu{v(!)$A{7SPMgOgnY`&L76T++qnU@1tprsYzuM3RT|nB7esv+3JK` z{!tvdIE1#yLgS94-PlpZFu{ZtO$qHVt<25lZhje z!5@(=)g}b?4IjqhgWsr+=j37+!&ss52wq@~J^4`V!J6NtbvYxE)H51U6(X$f0-0hz zINGl=sB0G1{)X)|ithGNIPdKUo|)keU74&KQ~UKNaHbJ!%qAFR^!z%Mqe=U9xDHPa z-X?Ck5|fihHy5}$zF_iuI@x1W!cl6TT<4end@yvMc@4XmXuY4Bjr?#D#eTW3ev^fu z3C8RdxY8ADbkxC{tqbAYA%h-lu-aK_9Xg7|KTqt!AlE!7MqYAgsY!TM;xn66l)EFZ@@XC zTw%%?FLVFkheXyX9@8)L7=ZU9j`)ss&^keZ4aJez( z`DN>JW9wAAa0?`T^{Ku@uKJ?j`PAHoBU}KSZSP)aG#JKtH~$~PboW2x(TQ~4?&;Fb zg-IJ6MAP}EqZetv0XX>zvZ{dJ@V{uQ3RqA7Gr6Gg_ypi0f=aDtT0khZ*RgFl3!i)g zeSlMKQ{n#140EhXsG(|InN)=b-?>agPsS7?N$uA zWqbiWW@xc27!j9&?j7z@A4t9{cD=VbA2`Q;NW>6P;_ot>kzhmQAJ{!YJG?+$QG5gX zTh#Kh*ogN%1~w^0kc0E}R@(&%*d}O6V;~CmJKw9aCDs$veHUKNGP)|^*D4|+z^aD2~?j6#p90)^dot2U|a*4S97_DdJ8$_|ks`BAy?0=*raWHUrOi@p7Y9rsT zG12}}J6NYmJD^U4Tc6z!5^Tgjn(vWL{&tx;)pDi}kof0Cu!^iC?HtRAp-G$lD6VAP zpPfQZptrjYy-&2|r04xjeu#S~^)LVR5m^eOm6S-qKF1;S5Dx9lpBmcL64E)5z#kFG zgg1g0MNtEGEXE72ff*8Sf5({Js@DdGO#~0IxAz)Pv(yHHJYF)#b@q8rqS3wj*x!T* zwax~P*QmN&B5Z?Li*?guEDAmKr5+V62R+TEps6KRd26JG4-1NrZ|C29)uGzw zR^>Z;%V%R~uTg;f$Z5nHB`Mh1IMmiPQBdsO}^ z2W(_Y{C2-!l}WILblXB9y&Q9AlpjcRwf$nj>d+G-UN949v6&{gL}Z=LB0}`E!cn)M zJE$_oYqV6}-0?5e!1d%LO=F$d3#?(s4ByygWUyS!PZ81k*Xf7@pYpC#mm1jtKPio< zAPyLsZX$!pt2R=*v7w3W2Vn?RT($ETi`PE7YRdQ2IGB%HOWSPiqzJ&*oYcgo;#oPc zUX+qbb@VVg^=+xMVL`nA|DQpOsWSgg+i2IIkvNx+DDXXf#vrsYHD~L#E&wv@#!%CznpG z9YGJ!L4DN2Czv8G3Szy|O%hMH;h;(ATyFp6hW0N7<2{f*dg*Kq;CDuB#^)e*T0&W_ zXt%&15pr%-g`sWOuj(~qCQ9>mqcve3fy`XXqi}h#D{9!;QC1TL%cQmAD6;buY_XPx z%{EQCN^EQaS-$EV>)XKc(Oc6m$1DGw*pZ2|lagyDTcfE) z6iea$2Wpur>vE|%;@K!B1WT;JD1mx@x@BCci<2N>Xp-zt%qh9|_Uevd1`+&F+#m&n zyATc_&lOHt$45i>+vOny<-HUCB01p&$L1YyShS@qjX&`%f3k+DA1A_vuwlW{v{D4W znZ{HQ>__Vm$~fiP=r7&Q@Zsu8XTG$B+U~bTDr<`bb=`1Cd7r^B0^`+aL$zFS7cy+; zz?H)zs4XkS(>ejvHb;5+l8*&s$#;>+0(6AX=zgZy#LnhWW7LikrrFlc(w&Rrw$?Mx zplK5k5(nIToeA9L0@=k#hx67eeaEX|P6v;*>*3|RPLZ{@7PiU-_*K0r;Y~4XlmiNr zx%`gKTRt=uzoiFb8bNF`CJo8J=<@Xj( znJ<6k!mst^Sij7kIW9BeLI`(fAE9KKW_rj^)IKu$OwPn>M`q;cWmO-@oqaGxDN{0| zaLI6AcXC|Z{?c0Lm~}(Vew(wt|EdCr(R0*IlKq;4fOtlNtniVE`RWvO+L6BfZt8K) z9m$3{E(iCkwEdVN&m*(fGwpZ8N_e{ODza>|8&)R^>bm>1LFYppaY?iCs@?gF6Q{@k zTF^DL3Avf4J)Vxq`)|5sT5*dTRYeSPs)Kn4gS5KhQ}5_m z;l2rHAcwZBH|Rz$ZRo6*^M`0K%|QpEf#PJ8ki8`@u3{f~ps|Q;`U<2=j-%#;f6VoA zx4ssvQO2AgQ#~od?34Dt>Vb`;@B%#4YA2dx{!klMN6V!+#qZKL(k;mCwjGeZ`C2!L zygen2>!%#CtjDz$6-k3zjYur8<#c>XF_&_BNVAJ0%w?> zp9m65L$oQ|(hzASMt6Rx^;hsNSQFgSi1Jk4v>283sb21NV1}k%3$K_pe>Y#x|Z(DT9+@{7zXAMpMTUG zSncB%`4;z5h$A5*A(Z)kZcmf?1PUmBMBXcX!0Gi=wR|H}&DbvZXbo)5X#*D_3$-y{ zHp!byyC;^y-#Gt(Ra0WPYjc~g-72slc*Kn3*g#HSn<`pgOp&MRn(RIx$8B;MlFNXxeL!Xf$S(Tnwr0vZ7+lJh>p1+8HAi>X_56?g$1z}GF5pE zQgZ1H?C;BMetn3e8>J5pp#*GyCmuiPAKmuWcijy-m!5H(*}Tt#s!bZe#a&l&0rjM( z)^B&r^O(NO4v$#tn{MscA}S=2YB!l03%^R7GvC&?Y?Y;E=(5SQD;(?rJJ$)Sk(WZ< z>QXSfjbV{sU5l|bdVinC^(*$OQ{hNpf|Zw_DD?LvAaBQq2jfh)?;7a!y%2PYxeUrP zPGK+gon(f({40v8+Tpra;r~ZCVKJ{fU5P5Ebz=5jb2RZPfp5GqLNq{+GRs&#uf|nz z_4@{-n!nXIR>*fpLqL^0X?_RC`?r^vBRC?2M-K>>4M-0OwLK?h8u~E)5<(gEJ*JHa z-K<>=^Snk0W<%%4+PjA!j(Z4ihUdPsh~@z{mu&bF!3p8q88ybC~c z@98vd?FiQTU4O&EVQ2W)mrP~46Xp0El%LCu>c zM{x(bfXzT!8D}cEzAl?hn5O&-ii6}w;ef{|!-!R^{Dx=T(>5=Ohb(!NaTL3fKu#*X zr9W;u!>q2ukt~s^`nX}}u)B7s&XXSB^PzM?1@#Fmi^wSZNUq+zZ+m#4`E+{rAu^9> zVE{WRIhpcsZ8iLN499&WT?p5IrPq7(?V*`p*~ZQcx-THMy-lP1K)P4GB+IC81_zcO zoj05)261sySt-X~sfPDK5@%DN3*mXvM!4slP~=}p6ra%@!$a9Sxs|aIB>1QE;sbLT8@t|Zxz=kD z`wT?Q2E|Uese{qUSe-~Z6@cINe?%JYtrzv;K&(z+l=qkY7!Yu5x$yz?!#`qwze_h^ z1a=)MGJrf|d=R#c=`KP69-m+DfbqP#|F#uy1QNZ|(`H8zzdi558p3_LyRF6=BCmS+ z-v74C-U}a+?>C)*^T#CeT1j@j`VjAtZd-)t(GxU!2YR)21JkhI?uMNL4IjRdK$^kD z`^=Jmuqr;%Oo9G25`nG1IQD#=-Ms^Cx-Kx5$$KF9*GX&- z%8dvb)>E#~OU((g$~^eU#H)0My>XUqhIr%J)-3c~l~Ef^ms4EEKzrka$VO=58@Jo4 zk`UQ+(G7O2sNh$%1pidN3Pclukpz!M_ddyzpw zK4m586ktJuoS*DAt2Ie%-i06M2M#Fk^sTH;Ty-8Lw{fIfBLYwg82(I@c(0u4Ha`g2@=qxqu z^3-?6T?HzSWsa})U3%EF0qqBUc%Go1yJ`YRpT_RvLNV7xlN-O|JvE`@K{m@k6!p`$ zId?2BlrZc2r$&-5Lu1%h8#GTk=*RuLk`h368EAyJ`Ak20SL23rLc%)1Lil=v0rI8( z;0X1Ve|-6nd$mRp?>mJ}yI&%Fb(tN$GM_bYqD9bp0jK_OY0j;b0Q3}gA1Wrf?EJsU zK@Mo+KcA0t^tb+gYdpp1CwDgJ{I{@@KA@)eV2N)Y2vns}%w5in7G;ewRE_j!7^*(! zb;7RyH7h%w*R7!>h5DGn&r;&XkHLf8S8^1xWS!cr-98oGJ534=fkp62BaFQI?81#!wqZ|Q>eJL*$Uxa_!Zhc-ucgt|?kTTM^RI3$4 zF!(%z6|y5K$q$*cXA55I2_M9RYVsQy82J>mD$%t>{XQWN6zzZSSO@Jx84);0{4-~M z#BsKbqbjKc30>0L@A5ZfsaJk*b?WWbEXfHVVbl>W9!OnlPkp!-s$hJKw8}^cLq5ZJ z-@}jT1Z4)xcubY__*c)s!eM49G(j|E>^Q~DDumKL00~YClTiY(t~K!1$dS7!__kB= zI5nZGgnN-Lx^fl+l&D!L$8kSp(|`y)tKEoqTVY0TQW=1*qzKX*AgMf}ZSxE2K&g-P zDuA&Zl)UyC-o8zU@?LH4Bjnxyx9#KfT@>=@M<=;u11fpGdkE;e7Pts6fcSmOq%5dz z*XhO~%$WKD$Hui#IOn{^^rF*EO5j^?Mbi3@&eFyhwQ4q5=?CM<{fOJdY|!^qEP^TR zaZ{osb|q$BeomuTHGCUL(7CDz4d+V%#{;I)q4}}`4GRzdWFgI>x?NctagrW&FK$^j=U3- zuS^UXVZCNCYHDli03xA{ISUybICW_{&r;Yq_-nc$K0te*lsmC=tql`T`GC0Qd?RKY zcnrY(5!n)wft~Kqc--}G)R_%)_s;5W$$llnEVSF@;xtD4o^hj%qGmN5SCoSMVST;U z*={VuKir6KJO}>R7T8Gydcq3``{alGmO~d-=m^*>vo|hEd?UzT2CMUF)TiFW+ zl+vwc{9$NST+D`kLn-!D=AI)|LS2WB#ZE$R@ldEZrYD7$5k+*Fk(r@+e zO+;=-2M19Qqt8;(CkX?HHmXsYtfjUclZWA$OYNAMQ`%M8 zKZ}}h2@hD?Bgdm}@%8HF3+e$_g*t}5U@|aqA==;-jeYq+e~(AoP^5c7i26)(r#&(h z^f7O;3I>OU6Zs~R=mS(;4ygwd!@Im=u#FW8kI}OX3bupcwU1{0h=bqHvv5llUpu4inl{Na?%v`B{4u7z!bFX|%h2fYyLfJr$@M1+A>F$Hw~;$qFeX3 z^8DDlogSe_p`vHI2csXbEvUi3RQ?nTLk&`;QT;f9PqiiTF-xFn3VHXS0WbQy7;yz$ z@+;O#f0dA}WDJ!QnH{j)8XZvN{!)(V@oCo%86Qf0UQMT4SlxyL?+X1dHiym3w+huG zDHGz$7b6)MD)Ra41WR5V-A%}hqRsJsCNK6auN(QUx;+aZf`EIXqtP>!TV)WWK;ygO z(0v)-U3|^ea$WumAUFh$1Y3^1N1y|P5NAB%yNw4(c0~5cK-d(YaJatS>cELGKwmJN z-uusHkT3Ilro@K=?K?8_`^XFM`*?RC;!Kbl=;k|!jajS_^uHkSrSCMm*Uc^03~=vU zgFAK(J4d87TR5TJgTW(sFaLwk!0UnJD8NWWKzgvR4&f&bZoUKP{}B)Q&v5Q{CT1~} z!j4fch>*Z=xgt}%Ab?Lw83xx1<(y!7yPdYH(30q{|{u%RQFymjpnlP z|M2Q(;QxB$@c&T(hzx+@|9ZBn{Q4i@VZb-tOA;KCv*1sn^3@MKao&&r#kX7sD$)Uu z`ldWE|KH&U=>HW8#qnR32KyJzao#6xF4=fd0SE)-6_&0ejqfUw~ zvYA6gNUb+y8|ta4Uk7zJZEtzL?wfRKn9iy&bW`rZgSgxcgLa>kxj_Qj<2HqGe)k7q z?|2v-?`r%$Be~H6btmNxo$f)racdjk3%%iVFUI4*S5o*;rE3=|B+zYsQjN~*)acb5 zxk5izsQR>^z)66wHaBoHPGL)O_ZJ@V`nXDbrsE$Q6kRB4+h@W2_KZSjCB=CJtKoZ7 z(I>I7CWQou3z2`@(zw@Xh}}DaNdgDp^r7^D%rXQI7lxfJlz#kS&>T#&j_w*@5H48N zg3t69+T2S&$7N=jl)I3vO3mRM>OLE9iDX?Q|GxW}+91&l1(mxx(SXHQ^Bq1Zd>Hc2 z6V)tz;EtiTxv*9zZ#kkS@|Z|f#M8=AkYUomewfI-pq21(jD${z5s!_qPE;t@Hv2cV zdeTNAkuok{lt+mbN02i5ABU}fLbkVM*6~mgt0ResJ6ZD$3%MgpWET z(59w~>$K$K3h+a7iy&CtA87xjKp33++&1*Fo2PN;jYa;l%zOC^41DNRvgEmfg+1f* z9|`&B?S0*(HU0PgKf9$<5GwK_2yu7!4|yE9N_)li1Q+s6rS=4eb|n8fMENMWG20O9 zF_eqf{(9pxAwY#gmx*a}aRm@uir2HyHC#d}{RT-!5BwSGs0l(lEr7sq@#h}*%@NWtB;a=kf3TMGP!okuMf+_7V}&?h& z_8wik*U@l+82oWn5^=~ojXraONlN&)N)r@^MhBMYDt{TsqOkM1Xl&LAC0itb`V98b zP*jAVB=cB5MG4IfGmZoiz3-{Mc9UG8@}8-d`NhwU%9T-e>@1){vMFPX4yW127zv#+ zrb6RjzBM;M&dFK==oNxov~x3P{Z+jW-d;kzLBxSiPBg5$R<6!2>sw^5`&oY5_(Vx&qU)^y3#IbF~I zT)Y_S_j&kURi6PjY854sEw*b!#iK*ODyl^T2uf`H3j z^)4Fm*0Rt3ExA|BCpu(M7^Ig%_oS3F{c|An)pS=+K3j{n$+9gqm)rU|eOW|wD7HgF zlcvxE!6~d;T9$NDU1l#AEs4|xUopr%I!P0ZXf1ZS?+&E1BPnzOD-p>`)SGa@MEyyL ztsG?`MvEP&W|<2CN!~O*akEa+OuJe?ZGLjNaLEc^#Lr5d6{|W5xrgL(CyCrjt4;?M z`KmaE;t6;e3_- zd(cE!7q@ebyDR=!r}DidYO!Kzku!E;NP?3+y>0lG1_FZnPaB9%U{3 zz^u3(tvQ(_Xrsu`$G;NIB=^d)E>C(R&w-DrFFNvXnIix5IecJ5wM@)z8QxNfdS+Q0 z1)Z~mt)MV=k2czqplPYAj3Wvox=?36!zI{ZlnNeqGq*AqO{>^g!9nU_ zi)fiwDc*}EadrKY!jzd$ZrOyE(CG0$&b-=zvamjjdWO)eARnfn`1?`3ao5rVi&FZF zRW5v`s$??=(un!~Z{GdL(Nme4QR-E=*fEC0({m&j{kmm11e_dXsm15 zWN!q*GPicjB^6G0L$3gu>4fYZ&Bw!ry3sHw*l=3xhpde9zvBY^^oy z7(5(=h`ePiu6JMjv&~IvB^Sw)H0vWff^@)nd6{$}WND&{4d86|ZZ|?rrA=!lcQJis zo3{0?K=KhS-t82UM{7xz9W1TuRf4vw$$G`kMH>1HVuH*Xht#58jEJ9E6SRoQxTdM} z>O5_|Y-iiEYsT1JVBM~sG>u6em05*OkO-K9+_I`7hYDbwVw`bg9shYOk|@N(nWeZ?YbM& z1`o6J-*ZQ!*ZsA<16Fx90OjH{s{+Y`xMqP+c(Xb>x9tofxp=tq%m{TG&Onzcfw<9S zWgJ-PJt>eSh2U|=!4{aJR^7lK$PWzr1hw2|N9C<+`e!q6m$NF#!V@9HnQOJe+CpmGdbAtXz(>ZD|+ z92^pda8M}s2M*~`R|iaRz;9Hs4EM|Z^`Y^pSPao=&#vyDob5n2eQ(W2NtH7<3EgNN zlZTHLL>ynd8R-rzh;QaX+*UZw zC0+6Ghy4zaTs@s{E)$ukRztlTgWy@O_>)yR{GDDsn~#R*p^wQ2@iJa+oCy4ee==K# zF0-`P&DjVqdD}zfEOC0QiuIa#e!?q&8do0XJT+mu346 zG)GLxo_+x?FF8x`OKC=V5R*pqX;!}WjynAfqyRvsEwY(i;HiUhi%MNE#5gb-ttJCZ?`T>WyV)gT|saWJ~ zE|$L>+=gDDFsT~gdec-VvpPSBytW1{;&TSr4>?pSHVN7u0TZIhgUW|flD#%^&_ zKxSZotS}pR^aU!26iL7l0>qamDeNz>XC%>{+f#RIcaA>T%JO#~s&I+caJAzacsTr# zbAA5fiHJ*@A7Nh|n{m8I(daoI(--QK;x#Y^OVJ!G-a+I?E>bcBSSg6(@p;%NFY@+w z%L5bCt_}I&!@73mfl0>w!ozAPGXIS#FQY8`IdF)w16 zr_wj|NK>_-yn}Bh#j^Q$Gyhd+dFo#SG`NpP_5T4yK)SzyYYO59_}JFNi64<$M|?d{ zGt{&vd!fq2ao#;YB6=3$D$y=MBgja2>}qdy4rO(Or#NT$Wj@$Y)%}WoartI>v1ZdabyeCLy)}s9Y2xyBFOv%b54+ODP07Q zDF;m)L?P>j8c-SaS!5egIiQOq=d|}*)lKse29y3!n%#^&I+X(GA(v-ympAMHwwRTR znx#r~(h%8gHst6(?v~?2=-8aWG;i))=8mmVZHZTdl~B-kVdQ7({=6zRQB&k;Blh!5 z4fWy_h!ntqFq7x5H^g+e5T&kmSj9v}4n*^Hz`&hzYQGt@`9MDiN1DL9gJT z=}dJCIAGB1)3@lOfJ3f0%S1J--wMQN(S~*kT#3T2PW=3e9PC}Yo~$~45+&PFk${8~ z%MU39lf+;xp^F?DcX4Z-bHS5(RE1rydN;YPvUI1zYQV;}4lSLqS(4lqzyN^ec{TXz z`C@Vb&dH^PxSwL6ufO|7E zTny(zT0I6Ny-6o#Spy$LQUbX7B2zC<7lgNF> zKo@|)D&|E(iKeJU?z&VRXWQ(Sa)v7oi0dlT;-=dQ-}~)}W~Oc%YFqGxMDCVPs>L%# z$Vb1`XdIz2f~a~$QfZ+a8<3t_$xp(obp0J>0e70+fBdZ8o zc?TD#)b~*mMr;99>KdqYZb$?rHom>Go~g*6_)WSll1T17?tk z1OFoVCZdWjlR~hGN)P}xj5ik|#&pK5qsD6-Khte`P1|d?%*58kg{hQM?o;9MdAbOOr3!$&j$OQzI;ey;Kq^Q<|A=#F0VfRb)^;PDqY2FDy z;;QU!cK+pDcV27HWLtiOq9|Cu2j+7Fj1m>j*g%D7yd$DpHs7sNpC81`#PevPNr3U6 z3-*}yv95T=t2{krtCluR{T*q=q@YuVPWNKufbNsCFD&d3?O8T!1RhWJCbM49iS;9$ zK=|p_%@k@iohj5^m?`(6{M5Zk%1%o41^0eHZBxl8VRSHau}XTs-~f!B;Y?y zPTJ`XxGI|!3R)8!?qgNkP3(TokIp|C ztmM2^&3_}ocyj$xrW-^!54p9#jF%ZD4ggo#s3>(mZLW(*R_;nZ5|O$uHskJ1&Xibc zG-+sx(Stjd5;e32e3cIaD;&^WBjPNCxd?NeTSL!J`nrAeTBZmuIUY4VP2rd>x1nZ^ ziP+vsK?)srJT&fG+^6*Hb}MD15Ai8Y_K9Nsqw*!mNN-JZGwL!cUzC7!x8{`tJuMCu zhLGpCm_bxY^n1&pDYns1hj3Fz(5c6dF!fv^T0y+s=1s{!%j*%M7q!0eSs1<~*RoZVSbG$6jK+MKVo94OHXAC01btTb<(azdjB1aZ zNFGz4n2#lY3pEKW;$)~oR-Q{c^I2VFzn3#HZ_OG!M1&IcFu@CQ(q*A=DDHJVx| zt6MNR#LmKjPZMxmSMbk*|FCO=V7yBaC1){CkzAme5Rg{`>@c}>#qyS7ac;PYKq8*V zXL+|+oqzXR#@$>YS{-mWPiRTMvVrQjAb~b69Uhk?%P;$}MJLO*`FFHau|eyPOMc(2 zA&yH`I6rhUhyJ_Nrp7{*(XyktI}9e4)dR88Wt|HN-O43%Qtioz08!p|FTpSmxk*kj zDy`Y0^<$(jT5k=+*6ts+T!G4f)wAFgX}}WNGIM8wJyQaPp1ldv+XU zU3P>@zFV?13V{QkeK{>?d+6E4_uVsmmbf{6{y`@N+CT4Kc9p}V>ExNpM>s)c#~iH3*^ zo7jw)Cp0BgoIyNG#L!2hahy1=+S_$L3!g5=tk{q>vopo`8@_j8(%bEZrdQ5DCkH5> zb)s&FtaS{GMSw?{ipgGXQ##wePkm@d=UGi_QCz>4AV>cV=H2ac;#Hg~Y6`BGR z>~S$7W)^^+B~WIS$=QWSyf@|>L=F`dl|K&qagG$Vb^R&UiWu}4r4l|fTbzNe5tspD zDZz?#3g#Bx>u5n=-((@I2X77bCNwL~1poUNJQYsRFRg&x{#X>6!0;u2hzsk6bz%;8 z&dI<1DzWM{#q%L*@%x<~$LZA=w9xdo&t;_<-l7hEaH;dRf0@o~)P*zs9IV%wD44Rs zIn45UE=5A6z^a5Zm3qN)-^XTrH-3_2cg~UmKuGEHMdlyLe^EH^>p+C?;5Sj-KO548 zQuj}qZT+i|?*DWG|Brh1KOWMB>f$$1-9H=Bg(q(L=R+O;i&}aA9=?NFo&R9k@?Y^? z-I{`Eo&^h44`3#5D6ygh;%X60kh3?F6N_3+3n`^;xjJ3atK-hlO`s%eOoHiTBjXy)fBVmDRf=EycQZ{&K)06H6-!fQ&AsGMVn3kb1hUr~`q2Yk$~ zSh`O78=?ycMeWn_47taMwP|-XW^AC#rxBFlLpKBw*&VfLo(r4no5L0GYuzhVp;`mk z#IpgwUUgS71J)`!R)V{YR%A!`pv5l=(0_Akt7q&FV8s>8(ZYEIkzIYMkKk+LJO>z6 z?G=$v#2mohCd_7MNTAFN+G1N;y$dVwl+)f$lO&!AQU@dvE4+w;UZV)YS@r@}CE7cV zCf0R}8>;e?F%70GvJ&pe<=<|~79-jgG0KO+Tv!Kr^w0OV8n@$N1k7=+i0IG=eqS~l zTh>9mGq)@U%AX*>#>|fGE7ubS{;}I*l4RpfKvXH@!u{YrrOvFVic%XYFO-akY)>_X zs^5;c2DWfKow{{Y@cqKEm9fze$v;0vXrNz_z~TT4g>NF9m?&nB_t-y)4wV76TFPN= zy4$&0S5doM9!flLs!I@V4M3U;@%*>#Pd`v5ng|unkl1ylKi1_v5T!@7kD7eGHWDRb z^OaUSHd@}ko!5)AOTq?0sg|H#N<9$prEg%{SwlVET}6>2sz%#sW+kgw`N9vKDZYdN zpq=zK#M*mvx;_m^1JOS7!~Mj=9<}=|pgm50x%C3hK(o>!2k5`VaG9BoPS8!2W`s{} z8&y&HnDEHN6T-aILWLc&8vfN#u&nglmn@YbMp5!b5wmPiD7WmL5hRgF6hdAkvqz2s zYMU1=_0N+qGj2j1YXa=b`!#SL+8hHA1)t>mhDsX#O>QZ7<+X(^u5699c)GsfvnR30 zHti_KCgGoUa69+s$9+*p&3 z$0H^1HLZ_D$F{EG_QWKcwZn~>F<#{PK?|!;d~7dT19ZI=hy!Z}HX9KZ*2y*sMHFN# z2zA(#;Z6XIJo3wfs-HHCb2Hs^4ZWVT*(03XDJBk`>~DUZP>33X2tIgFzG-oN8}j24 zJ^LfX7hC_dFJzN6GJY%eCX(Rj-J>R@mqiwgZ%QhjfUPp!h6k7*k*bJQ1CqKRR@9)v z*_gXRDXxneP<)jo%Cq0ruyqJ7Zkq~5#4_VW`?Ek0Ug*_<5c_iY<{qvNyXoCLG}bTx zkP!^3$ol%$>%p_x+u-AP^UZunnL^a%)P9JxYt~(;Mn-V6@&xDxgmj;FiwVU$8+!Jt z)rpSaS1Lo}dxwQo2XWnXlV4YcdcJB;OtPC{mWq)n$nsn)!5~Am}xjVhx>{8C>_d%Kh(L^=twjK;(VRl0Jbazw!ltE zR?^|Kvuj3?$Qdxfsi-%=?-k-5l2F`0r0imL3W1k?})po>W6qI)!?9t`EtxrwUzVdJZvKkv-_N#p!L=H zeBSXIa30{}>cWpMUp3oAnRv>-t{V=1Sx`}K^ZwNz2o71jq{tY^CR~T3+h}jayH#d@ z2W3&9M@mdg7ZKTbpy4+J*AdJ`aQYpw311;ch8)8uczvS-S421T>25Q`L`;K3w|V@Y z00UentFWs2ti;dj*(K9nnvm9A1rAusevLWXvA8dX)#c4osf~*-?qkZ6Ug*1Rul<;; zgMjcjQwy$a$=rne_+J|M2)&e?@C|IQ(svj-RMc=DIuKC**J0?Z{Kce#_4i)N??`q4 zWDx0wqGk&ZENl+{ZSJ0`6YT39JD+(ynX3T32Xp1M zySxGNpPvim%J)5`qUv&cIWc1PrR^2U{34i79R+Oc`J~WX+E3M4>|TERloCppUpMgf zuH^PaoMjWyltSYkO*9OpvRuuU@>qi`ye4X~AsLNK(mHpu-NVJ70GmxcBr#S8iJC~G zaNa|Oh+jy)BW(T|tqaw0K5}*(EV?Z+QHRr1Fs^;7VKlIwj`(8hrYq`qm@0ep6A}fCBW>#;0m`*a5p=8AL0ZXj9Sk3W%82 zEOnnHH@-2sE6_?4vQH1)#E|t1jnbzd1gqOoPpQ|f_lz)?1bI0Qn$BpwEA-No?aqvyyKFLCtng@P~uY6>+x+^Wj6V?{|e z>m`Z8NxZzMZxZ<9&fJF;g#mDvNKpx`0Xyg`lDZ?hf%7J zkr`2mLur_SC`xez*qwmFq@rCs(4I8!woOTY?>jazA9xZE?N?_7R*(9EwaNK?? zV*==r)>)qvLeQoANzllp@?ug^k=W6=9NorA5#yD4oRzD*36p^pT%*SlUjc}dMaeXt z%|+deD$Dl!e&Xuo@p7wtrpS-B_qz@}y7=1YH{{k>;j z5aYj4S*BVF}X6I*Sip!?L0f+jGO&SGYCmxE>w9*J6 zaj;Z0CF11z=vNLOR{TAoRM?dD)MC&4sM=8}r&W}-Y2KeFQ&Xe+E0QSfbpOWVbU3F^ z1(a}E%8t7hW%e%CH%$8p2`);ykM<^z7unAJMB`vzv#}BK)Nng__hIWlLybN_Oo%?w z_&1vNL5MgJtK9Fgj!I!jiYS9P*1*e-ipD9>K4~En6-ibDyu7FiDygyIMtJN@6#fu~ z7L}M22JDcj5$K(?#Id?!Tl7B9sLZZ1^;#wccen3>O|T$-qpx1_4(P=Iu*#3^XCn`c7l* z1-X-{&8Xl$Ih%$$+04--YS4C#7za1x7O`pU&}S9$cik-1hPoA;!cAu`$)%!-IW==| zox*c#(X^LGP$O?~w|z>(hb=$zgl_8j&JECS6J8#lG9#^0&d`fWVVsl$L*>2{vWhnJ z6?9aM<&-s{k%#A)nY+=?orRIFdgoU}UA zeqz%tq+%Jr9<^*vzzX$QLws=^0(9XGHp;ifM02mJM7T#k@Nq&r0Hl=Rb*pslLc8+w zIZ>Z_LdQCp)3d^q(|3{=LaWEbVIJ4hU~5}0z`OrNL=9(Vtn$~BFIa!+M1%_NU)m6% zf>WHo&G$IrM^6Iz@AHlSQJ*6KV(RJt3O4co0+Z7JE;;zyaPlwt!GGIN3@~YV1?K(y zj}tro$yB2PMQ{IVMA3g4llo36`m-a7U@tFNq&Ne*+s4jwva1yuPPz8&sG>l#GkxdR&`TH!tKhcv4hv?O$ zyV?_qL&q&q@o$l!*Gq!J7nkyl8@j|k=oCOHCvqvTzAFVdbaNyl;IXyEs1)ZXljd9# ztj6eD?>(!WA-_EvpQn&WABt`oB6>NzXlsylLvZnzaEr^?>jcY z+W$8_O&L4v=N#;LmvhEkEpeLUYDHffjc@ugO$3*?y#_7;cr3|K%DNRwLnRlu_(>|{ zh)Q$Kj@Gd2`Ju*k*4gY&{#w^pah{FWc5YJL8Zk?#_IH%h^W`XfU?riAQ-NLRz=P`T z!iMKJVG}!%PWLNwYF|l;5CWhe=kx6&tdG-J7U`yBbYykVohuXq9zPGoE z3m3g#D=ghS?or66qLQ~RTQZ5mb`!e$JR@jkQ>hF9^Cph-Kd7w_1QQ%R8y4&(DaNA^ z)4;7$`K>&cq;1EbfAe*jD$vnNKVl-k2cps3tYh@{%Pj5}urY7HUbYP}boNpJG;LEV zB3!xLt$WJj6o3jP9QrEB9=H9$VV`Goe5JK#tnd5YiYr!s{5RmGZCPqdj1p5p&`yp> z`f35hx{-+fgj|MrzLJQ$q|5c4)+8>SJ^F?*{CMQ*y-^(n92M%OPJ77*j-WGFEIvD7| zJg~QDAySw46a=7Yfg2ZEvGu^Vlk33A?A04S%*;-Y>7&w*FdGm52(jyRGHLk=wGG(~ zbc|e^Lw66L;XYR{q6Gjjh_XPW7A&f-J~VQQRv&;n#gRD8C)M@YCF!@oEzz;StdS-j~XxGL#pBh=mA^;(YgS#y?iL5Af00}|#rRuMPyRi^a%g;G2Zq8{@`Z*N| zjJq6U>ec7ay2-3+8f?2&Mt0?&wj79dtvMGp(GkE`A`1g4a)z4?qb-E?+)>ixOW(kxa+}mCV7HffX z6fpd5Y|?8n2rV!Aw9$+;SksuuQ~#Ts#X z3oy?Li&CmWgpNyX&UtcPlDld{pv0P7`A?0IPXeVGH0H@?dUlue5fDys06gz_C=?q3 zPQKwC_ceEj61%$3sHd)1Eu2K-A(2ejW1&)BErM%qOWn;Gt+p+~WX}5r6X}KcyiuYr zY7^OMPIfEwt=CQD80}QO)y**P-IU%h0OAK*v6zoRTgBEhxcmm`e;bp!Gz!IQqe?VU z09C1aipe#X$g*Izt3r!vW_#k&T8HAZOeE1HAB)4|*BaivU1dhb!5dzVQ$#U^vUso^ zMX7U&TSwHDb9}@=RkzYY6d!`yB%2?nE4FY#pq|H^=2721t(cRmH>K8$y61%J#)2Rh z0ydg$QN5?$#mJ$e3zT_|r$Hf|Co&kI(}@}KA~9@h!*=Z1m8XtJ-{NWH8tLRA!AQ&f zPUxf{^G#x*j>|(sWK%R^6a*C7LLpgeVQ+eM_`Jy_jQOT;3tKWBI-EnM)VsDSx%Oqo zW{_z&AnyBE-h__d-hv@=LUqBeF#fKT5_RImG(ONYL|k%{(PVg{af6&7Vn8MB4eT?&J(4%yCrRl0 zJggb9=60ItE;)tRc&bmI7Fg=1q6_E^`I8whf7cS?UnLKHQ&Ru{2Ue}PTFpv$*C&OeU3{Db81-9+yq?p~qm!{!GV zpZ3$8smmojwn#KZ(O?GKs-yhPK0WC%ic4e)6e;6sF-+0nK8mqIN0^@Hau1+)TEIc> zT))xju2Bf-MZu;uM}E6E7C(<=74MZ{S`Yr(a?la==F+8hV6BkOO2w7}pp7U*_qJ0e z=^s5ACSVG|lZJ^r>6WG|5p6{-K z`x1uvEPhf72TfG=!@u2!wXsMD{v)H=CDirj0AP-~Cp;x^=ZHk5WvS9+ zDH!}Sp@{}i)@fXy)fgf%9v6-$s4yxdE6({MeQ1~1&^1uYel7?NtsObtCfhg?x4Mb< zUK_&775I;EexdP#CK4hcKjuN(T2VGswIjx$?U=q!)ExteQP460toI&%E<14;o1CX~ z8++>Zyp}_II-%tT-+LkV0Ad%MG69rvO2eRc#8-!N*AQPXMn!l`#@FXwy7vfUx3%kD z4gH+=42#Omw`1hfXwnJb&(?*o zutlVE2`FsBAWjG{T8*63WmPlg&aX#7^txd`E#H3P;b|}3Gg4Sw&k`If2@i;<`KTCG zqF84{MOmGAB4#0LFJ?+|kz=~SSX>1QmY6&RpRsv{iD#ImfmK2i;}D!=dw*mSkcS8@ zy9U{U1TiV+;x)A430G3E5fJI6p}g>hfp{I=#N*>WwT z(!uC+kP4k{k2AkA<4Y9i{UQ@O8|XyN0$c-t%IS4X&pP2b(^#pyj@F?ySOqSs#mlAi zALD7Hv}bj_Jny)VVd$#rADh{dBfN3Q3n0ntu*I`8)XHMkD)*3l!!*uEKxi#IapS2N z_r&E~UPd%2S7WdZJoUg^2EAW)rpnpZ3j?t(y^{2n>_XDN5z8gQv4QL~LU z4`p~8_TRVJR(oUy^*oJ@`g72Y@zWfJ-n5bYdLw{` z0+eY@j6=zs!HASR7FF|}pI);rZ{oQnu6)13yAQR^in3&9cm1?wZ=h3Wzg@9Z<@vE{-duAlmtZ<5{+tZ&OTpj+>=336Lq z^j&j-4HLBwffd0*M5vy_yh`7zsWBb4tk@ROo#N+2XXh~>cHyBv=1YFkpUYdnfE~W0 ztV&kw7Ncv2FO;N0b>ZkIlNv&HNk92sKb%>!Gg@kf9+58R=lOIWF!!>QDtK4O9>d+^ zU;WxpS@-Q@mZaD8xH)%Z-;b-2CL|fPJHd}!-@W{s83a;UWKpufn1Y*|u8KA;W#(X4 z=x-VIHN|3+ZK6VP8$i75%cGhAvS(WAfx3-U-@LHj(M_USgV7cyEUJKWGaXS#=(ECL zUl;dsaYr5U>SS$FO*U!e?!7ggXS^~>anmW@fG>`Z#jEP^J=7my|ed>(~M!bo6LAw-*BO zDN=J~%lF+bqH5Uc)IvE0L3MJaVQgYjk@E@NuAGh74uBJ(oQGUo}Kt-Ge<8g?TEv$vEK)Z!~bF3YeJeRD%=!t)4NuTXxHTAkjy$GY02UJ4V zap6qyB_Ii0`~v{ANrUr(li2?7xEgz8RNlAEIWyZ}(8^o-gob!l(=xAkYeC1=^LoI# z8{`{gl6C)dn93+Cd)DvsAjtgB`=Z6G;wf-97xc+T# z>wKuvzwCp861gWZ6%kjnXqac|)VnXeDku$=$TjACR6#djM-r27xF|oh&_p(P!Xp@| z^h9^Rye|3Gw6IeyynxONYkE6?O61t3fg^0*c~Q#X_jUh)9fYrt0&mv^k&4@`K;@ud z;}b#BD56XgevlhK;5TaE24IT9Ig$CsBIA526o~`uey8ALki<-T??`6N zx9+?0)z?$yV{_!SybJCzwE%;Q_uMG&Fn~@eG?32#1u7tST(C!Ui#Wv^as~5F8JEJr zo)Geu%o^dsnCqO@t%W(E-)1}(GVs{07V=$m+%iO)P^7}I(7j+DNyLxS zlUzyV>pd_UYd@cLX{H-lrbB|hwRaN*lD~z(N+SBX&Cdn#aTS#Hign_|9TAF3d_AF= zC^&q{>#YRAN`0{ zp}gK)1g32Yj^GqM3Xrz)28Q}a5ONjaER=6EqHky5Ki!Y$fB5_VFV8OjCeIQ^#C}S( z{Iwqu_hTH&j||OK{82~#C;2;yhW{*}_+MH={-*aFvBBEEN~iTugg0nG8=Y z2r!~&PR=zW^Ei{~9~&7?(Ly}WTREn-2x%f1{ve}jp)Q5IeEX2kg^UcR=g6#7SB^$1_QCyr$7cK0lA^SqXxZ&Dz=>91 zDG?G^3jh>aG&xZC><{EjZaoJmtIOBJioUKuI}07MCkx=h7stY9p3*GMB15w()3&|o zvAsy$(ngR)&g{THdwv)wU^=-JCRIjg0zgsdRz1}a!3WSo1!1tMKlecOTQh}(!(?2D z}0 z&#-IZYy_Y1^MK?!tTL(4>B3|P)*;x$IW7?`(%drrXC88Ax-HWMZzAZsh}F??CMspP z#`D|wHrW31g_;E(&Arn;j{?BG>)5~pTeR-ZhB0o79@8MZiQp~_L2ciyTP?)x zt9j#~d6W=6>YT^&PDi}YE>{{JnK+1P>8DJcF03Hfs3!>3q_bh`-_zf%VsP2~w;}0lR3Qt)cl!a~n57kIDc& z!&3PfHc?MBnB7-^MZw%DyRWX703*HZHV@0q$+?RU*t1W~x7rH3Mc0Cr`8*?2Dc7Ao z<=%|K47V%))cLXBHZIT+vtKLZ>{cNl38!ks>WCCB#fUIHz!Ptt&h^!OD~)$Ooa8*Mnjaf-t+N}FdmgL}JSP~C zhvY!yrJ6}Z$&OD)W}kkW670H?(-hwKC$G-;<-DPv5lvnrJ3p@s)IiY6$ka-r+W`%) zzibLuB!FeX;y=eZr4J%3i6P9|SA|=+@bFGtB|E4BuxMVx16y$q$3xU;WzjoON?}wr zmnmK0+;O(dy8opd`!U32S!PJk=hDpJx?jp2g zMP6O0?Q2B#`P+4E^oNrb_jpnX269-nxcTGj5xJdq?{^|_)@!3Z&_ZX=1smwwg^R)2 zEmW6(UMYf!-`X?Ts7uo<-)Yo^Pra#P3hKiOljxx7l6{_Y@}4?NfXGcLpqHdx%IB5; zxKx!J3&AUBoNxk@L8;3&>I&GIjV4i@AOP;R%iVl7z&oW?o-h4SZsIcuFn8}7?qg$szW{?`!CwD5 zoR|8;@j?~|?!1tJ@i@&D^eWMBlQ`2DE{813g7?)vu3|Cbi>%-)J3ab@Jri_GbTE7W z)ss)bC8*cHkw6il#D>-}7w|4(N9C2;a&6FIzcyu|eA`Mt=YxE1v+y|t1HRl&dP4GKPTpn|4vArPdjx+hZ>;cV)t@9{B0crYu|+o=`Xu}_G9 zUJi-WWuc1GP4!(p$ML4o2Q4mE&)rPvDGSqeJbZ25JujlJWJu)PB+1s4PeVe;i zN}rwm)rMP0&KdTkY~y^p320i+B4W9l31g9`LPV*8p}DK8rTfO1K!%P7dA)eg@ZweF zvcFF8**{yTNm4)n-{^!FQ<%%*7t^BgfM|WiUfJvAj+J=A;WgDJXCC(iy)8%egoU`% z7``cO(L;!Zhr+t`m46>M=xiiDfXBtlmT(Wmn9K@uH}%B@$6Nal(;RYb6wLf$E_JXU zpT@Mno*c?a(% zBZkj=$R!%4sF@FMus-ccSY_dKzT`Q@28gz>XB4`JB&pGBm+$2@8;Vus1Vl@VHKwju zLX7Em09-(uapvO(a=a?hHkHr5ew5tPxamsAtVrr6VA7R{t~X$ua|b1Eh2pY))IhJ% znqG!}yGvCI(mzCjIUy z_b7-7ERq_pRE4~x*u8cJQ%9%tD)Br9-FSwSXEI;}?|Pe}F0 zdpe2TAL;JA+VEW&1GEVr0Xjl0!IiO!< z_2~T~B7r2OYa{+#m2yHH4>H4`S zK2J{bF!~!i#s=BAqFPK*L}}>g6=U|z>RkA@yjEO51H4ZKK+D_LLO;JfqGNU2iOI(U zsK4#vDm#mju@S6$*>)Gn_l0jiD@2+eSVV2G9)br{50z`C!DVj|ipm-6AY}-no!-MB z*4*fS{NXelpZUZC%L!2>_MD&akh-Va9TB2Ud`jh+Y>pdl-)SSy6zVlDS&7bJx5>IMgPK${(P1};uX&hugMx9SWccFNu=<1K0 z_Z?zR*-O!QoK&bh)-@E0EnAjH7{5HmG1wjiYqT2TgogcT+v{nI457X8nihU1c@0`y zXfS)z;t<)nAo^`jx}=cpb;-Ph&BH;Y+74LUhI{w7?mFdsb}qy=y-pNdwh#(R z#0ycQiU9bm0su9--9+zBJD1U}!=rzCC((0gPx4zqyd0lUD&gglAXxkfZmP4afWWSp z&tNv?=GrTh>xGob+m-`LGE|+WazqY*M;y?^dnRm%Y1u%#wgT$NXxY<%f(dBF@*k4_ z6x1U(9DkZl_z*5yhUSEH?2-xSuX9=Mbo-q*QJ%0klN6Cu&c1LNl(eslXwnfF8CU}V zi`sr=<{KUoFva!Bqtnh!dg!0kZ)GC8iBhbZH#VB7+g)x?td$36_cLrH^wTj5aRE5r z89HLVbMXxt-G&pLAx zSgyf0x?ZOqoO=xKk)IMVXu~jn_{OC_kZ5U@D#i<7gsm%DKBqJnofDd> z2t-zShswidW{B+w6^1zWHORbKormG(+WqYs$ye(Gbc8)yvr6AS`Ga=JgCc(|r;Xsq z0Fled9%vB{k^IcVt$28H)if~7l!jFQVEZ<)G*5WnxoNb@-ZV_}U?949k*o@9aVzNi zFadFq(2~$SS0O8eRHwwf?=V32vC}E}gi{Ns+*;k=vqih#_VulR35BSmqDE39B6ysT z<%l-XAN+mKjmf}gdWr2Y3GVMW_35%At{>eXOGktLd1pE%smYqhoJ8vfFRn%XZJQ#X zMC;ZlC3YdonTbiAiVK}xrd=!N6kCapAi{u}uiu{7lNjfVr%7k&ga!mGHxXgb?EbYO(ng|foXtvt{S4i1_J=M$u5=T zy5Rr~6A2xMNakB>`y~FxVHfAjI*vg{&EsfG0l*sY4=NuNKxwbg)k!qsU6je}a3+vb zj|vjZaY+_5D&XQtlo4NDG3f7twW3u5TGt{`_KZAp6%Y#@98Ij--aSF^>7geAhzQEs^89&b{49|Yp+nUf zhci$Gw{oatnG(b}J%_{*wuFd>>y^EG9_f?P!9F1!s2$uR-2Ka7}26?dn6eZV+c9kpIh~I z&5MOKLb3TyNARIJJQor2ch+5ic^6RFEt;>zAIQ*5==SLVc^@6{Ky`avhz)~S9=qsM*id&hl zo-g5n^$HT3-`BNB(wZU3$8Kue6QSGg5r>XA$)eYk^4F}IoM=llSj~Lp$LMN*Q&tYU zW-&suOc752k568x@|>3MsM$hIfu+$;eJ8Zeo zSa;8wW^s-LG#impoz=*i-4>VP@pM&y{Ugsf{m%eIpY0>%pO<>$ z=o{?-P)ihJp4*wr6{ytn^Rba{tr!7?Mu-Wz`zC(1COV+gq=z}HmuvvrJ%6*^unU|g ztZ=SaH8-2?nt^qidBZiHc8V+DWiTGs9h6+ifIT(a@zTp>?bWyTM3vGXhG`NXkx|dx z)AxaJUo9^k#xA>zdRJWvn6e_Qr|jU)3oI{@Q2@B*eM3|iwt%$Iaae)bdT_xKBZcFR zKHcWYEl;;b5IVW$>>W9SiUDDOJ9}jnN~|M%ALFU?D162I$@l zupiATL1KmxVzb}GbIs*UsmAOcCX{awx4D1Q513zfwP-QQRc_Ta zO%{gUDVnkn(R$c;WbF_PHq4O$EZ=e+2<|4-cuka2QD6)K8_WV*Bc88glTcCat*({! z^fh8t*y53cEdqG(Me=8AJ`c3?1~1hli9inG`HEWTbUI<#q8Zrf6{e2^pfu)k(jVxF zJ#D5-ykAl2t`QY;8iB4XreE0e&Yi;PlbVhLrA~Fng@`+A((bi1Sd>Y!Wk&en?mtSM z;s7%#yL-|j$Mn{>kIvqsCh}293EOWQa|Jm9HPm+N(tsoHo_5n9#9E2SdX-9r8fbFCU3kZe`Y2XPhYtv57uFvM+iL1uP;;;(jWKkHay4|Hwc-+t4MVt#svd%hn&8649 zlT7^udon*ipYkpU(`yw(Hek%Kcxzp#RlpP{=KB@_gx9Yd!6`ZYBs|YjyzEfM2QZB- z6xx1Hus<5bgvAts38UJa0F&Imj@ECUJ1jf70a5wk@mQsAp1Yt>q*}bhpBobW+KmWW z*xzeNRLZ=X{I_N@|LSk&c+|@ zhyGLu9mU-L{4VGKk+@q7GgbW}e4!NpX3Nmzdb;s0E`;>`hl~5;o*9!Xi_=MeKNrtJ z`wWd`H1RobveuDH3xM@fKme&QX}Bw@A|rBgt~gx$7II9ObulqMhD`!gSr1m}pry~{ zbv~sd>=VH&fm+BtoMIa{0FEQ(qAu&qJ-(h378oT|iPiv^Ijp9i2xeiTaoM+BI~$EW zo<6}g#ackhkKAE;gggMluSE)jz${2)WbZh@oT-ixsnbl3DI2~suNm*=P`ed2VVT=A zaU_r1t(rW_DV74kON6tl9O& zXihKgc~cy1ux`-4ZtJj<#-`O+BxzM9Z2C!I70iR0|*9l7OddW_bMlW_ZjkinJhNiR8 z&|}82pBKMd$b&)&4kga-Sgj*!WHgBuH0JR+7q{*rxR2-3Mf?Y)csP8oD-h}4Lj&Cd z<|*ROas-=&Ay!Dtg6JmX={$t>zV2{@?u{!;D)u$%DDw&(mtq9yWktTNyotM8HA4YV zV}$6uhufhmhuuMDq1a?}EwcLAmLXmH#nVuhj^5$qIZ{Nv%C&V~6Ce?5v!vo68 zTkZWdk#GD^vsLX@e|cWUovqqfd(`u5;B1mpyh&zLU`kfKKRispfhG z_$$>3vf$qwotleGf&d9ATz{kpaEa&*&Bd|>#*?egNlohE;s`Q z)GBlxLvntj>>7;S(mnvbmkVVaIRVo-uqMDI&{6Y+=bA$iA&ItMdhA4 zGZnRSX|4}cnD<`T!S@bcXYGBCNZrm?5q<5R9GCIKnd$fx8zS;?a&Up$h}@}Ai7~f$ zfpc<$z2345JTkH`*^zPY&Gbsk^izFOnhdxIU(C;?IrzTd=l)jIE*A|w_=%!XnPC80 z*>oQ=Dd$MSZhJ`d#3GJzYVsShpTNP2ae@5opG~GU0&Fs2r%!xDi3R}J*OC35t9y1<>bQu=+g%x;7y!JQ5bcF(?n>Z0$z2%RHce?F!5>3VdTPCu zp$*;YhbRk>7`tam_IvU=h-v4>8t2erLl zXRo@J_U>O^rv6k(=s^MCuqhA_mgd`VQq~x6WQCD2V z10g1mqU0Y;##uCf={bXRu%!aW4C49vf|zO>XxWQ6dFn)`%IAWh=MxtoRkZPFdL7Zy zrE*SAf((%cc9Xfl)%i&@zcbY2LAv{q@;E+yZ#oZKBa|k4HUVDW_tSfQ-A;kv&QiS` z7fwYdBtK=qQlQlV)kT0#w?eW|z&~(m>(u3iyr$g#fMfsg&mV7{8SkZ-_GdV0X%gOM z<#kQGUW;V9pwuO&rwsnC1GgCLJ;n}at z^K!J1jHDNlN3Z}aYh;V*#T?w#t+!9ay8ZS+^o)eFMT|C7dw!`#I_*+|Skm~B_7ybH zfuebBE2OyfAxvP-2JC?IE}PF%>--0fdOc#BQkTKnI44Ouq_5Au*TiJ%LQiS3=YiI@ z)n!Gn?okKx{n!{m$Ireh-Z|lc(rCo2V3V8r~uXk#p=`4Kj5gVUNbUMF*s&|+-^_Uj{sF^jk zjOmV9NO73PC5+uu^LT5ArQJSM;`q&76u;^BPfo&Rw0xNyP)Q!Oa9*M#yUnTTLcJ21 zy3TSM&9fgdq4Rc5&x{GzSkgsd|Bh%`=hbk#QuV>Xyjm7ev_x1CJK&+Le{OTrPH&Sj zyI@7~>KS*wz^=Eq%I!PRmER<>_U?7B&N>a_Zrtwo_#(Yi`cnamub^5%4M{hg-{|;3 z1s@>ETBj^bq7$;kVR0X|;G>v3nh>dzgvF+77(z@EosT679N&5C!|V=Zfq$#~fJ~$X zbbJzv_JcgZ#aC;4J6RpA-}&-Ww#Yh2oQSxKxNI(Gg)ZQ41v81mTqYg85D7QcVcl_0 z?-e_+*N35#u@Ya|66Y?6h7zMI0z?e661M{HFT6XuGFi(jPZjv{zz5I9N)j+~ZG%F) z?ZP{8>OqXGmw&sfs?`Q-!Ai?^2RcJQ(ehbP4$QL<=ObqH*DW0P>#1gG9h%4q(8Z>% zuLkM+=QZ$J*`y847i8W~EYySziOLttJqC9j`>n$x{@H{>L0xwP=A5kfBdMF{J`(pd z9f_?JmDIYdyQf_)o)ypoD{}=p)quaQE@ayXuF9FuOjHO9UL?un&rKD@!0K$Lt?>zI z;kT6;$0y4^x8`VC0U0a{HQ4aG5)Z)^&UNK!f;)dh>jOF5g3-D208oU!P+8DC=OL7+ zIB4=vSwDfTc0zc&(5A3U2~sb_(4mp=6k;K1$-J*D-s8$jMU)n00x(lPW})CGmZTCu?^D)%#Oswxl8 zR#+2FpN?Xt$*C~cDuXSdZcT;mJtX2r!Vk{9c6k59T8LW;*S!=?H%&7vX<$5B?8%_MdYRAPM~`9pTU9BK$S? z;P0FG^7_BbMfk5G;D2nG?a#Rgkcc%4B8AVMX!8Y~H0IOZRs4~kZGbAb{Z2*CSnM&e zy`xRUP&mgI!j*q6tBoAfw@J(KO%_Eh~jE%yoDq?F~A$^bt zmIyAu$a%8FK&L}GAdMt@$rti<59b~c?Sy?wvaR0j!Xxixc1+?H$;C(};6+U>%x%-M zJ7Vao`VHUb%2KM_IclHQ2zdsU-Yc%I0j$<`gj*K)w=@=Gb39$>?CMkYZ+rf!_hc`N^;Np2!+2cDqB{KPrY^Cs&QhcAguPM7OgCFO#R}IW9vgznV9_W^g_9n@SX5i@#xtA0Ym~I zDGHJGH9@TN2XTvv{%8Y}zRoj~6FvCc2%&-A%}kxCXXyZyxlrV7zY5~>A$i2go-Pkv z%^e+vIg(t@6m*g=ZZ&c7qPr!zLTL19hy7s0J5zfJy10vVN-x1y#;r#f0(l}susI}m zM|Bz{N*KTD0xm=q73>$(HrbgonB{eT6LZTn33FA9R14_}m#^=uBb9cV?fYjCW_o|0 zSutF2*lo*Mkn0uFfB21j{f5oB7=ObL6TLqXsapWYEzt4BSRMM-OEhrpoSE$~aS<$j zW3w@|2XwqRwrVu~nX|;kcjKgQ1$CT}6+LSzeZ zb;}Cb>9<3^$+gFB1Wta8Tl9R9w`#T-S}L-D4p;S(UE1XJ%psGSb0Csvaz^hyq5+D6 zHa(ydA7V*J%HUEa5bVSaCUNd2IySmv0!&Uj-{g}T!)N!#(`CDh7eS^YG|{v;CSNzT6`XibvCeEImr^%Ygek2Zv(#GT85$YBHrit;*C_#wc`NfqV-??Hnnq(tO z_>S|K!-!P|^}-U&)k<4K-X9Kap4|^*Qb_Gq@ps~~SgbF^K&vY?C*Gjf_H9v@s!o_W zT2h3s`}Fo6KE~P&M<&kaErgDju6ABr7LvN@t{LYR#J!Mo)_2*IWUmrK4*7_vQ_9)N zltkw)nO*^v`T(rh4Gf@Top6Q}A$V{ikeB$;8zKwqUgJFsCzMmog@_%Qv*vLDlF`j} zb5k#L=D4!^^-&||o}JIMA-WfO+fOvf8n73Abnr8aj<$(1>W43)g>^_)F4hB1d<>a^ z^M9l&0R`>q`S`lO`3KHCA_p6SjZg)DM= zzqD8KeniXG%;JNYJKPbtn_QCSmWYNDi|G4`GTU2T7ZA5y>EZMjE`&v~}c7lJquAO~g+n3FT;fD}SHFeDMMUk*MRS68`r z^N=%BTE1H4@rbNlx{T#T#zmf?A_|<~dm*N)jvRPVjPTiy3%S(M8hmaoH!{g&Yy5s3 zvze{!L{Y-K$%4e`Zl||!e&F2B!2GZVQGB1tpn+cMhN^PMRnW5W=&qk!IY2*kf5j{s zxedDZROt<2l=M!K88C4tx3LjSLOx)_p|&wOX(8M~bb&>19$cIl^N8rvox3opRx|dy zMSDl(+s1Nf``k?idzY2wu3kpCbp{d_1YHO&>*?hZxF!F9)1S3j-q_Fig_!J}$SWd$ zUYH^!c5m#T5W|<$EZ^UH8Gs8S&3n4(6ie615DmB1rh-#GOW9E`?o8gI8r>>t0zBeJw`d-Or^Yx70S6sOIpW@jM~okV~>(DBYBaPF$a)?Gv?6>)T;QYflOEYqH!}U3!== zFcec$Ko;RX--upF=NdrFVlo&}jLGx3O|Ke|U0Tdw7&oaury99AZgrvsnK~COsh&`0 z9LSWSPywh*U{HyPe*oAq?x+@~R8i>c`hItih&kyf$D|x`{6Ig9IwNEC^10K-g^CW$ zAmYpE)DsN_52$!vs3Mn^`uZ-m2Vz1K*+iSkg|32>YKYD0X671Qjl`vWF$O_DV|jnl z`)3r^+ez?nm%S{2U6*iW3gmGIa(b4zH9Y{%+l0x^(i^i;#axWs$oWmO^uqlB88mF^yd351DzKzG}^0qz~EzKC0dZ5sI z5Wpsi4V;APAb507s!!Y3#}>VrB+J5a#6*lXZq4*6vD>{2?{1A>yZ2M`NgblfRVe&y zLqBjDYO=8eV^ z8b1)VXKlASvz(2}xkNvEJiJE`UA&mC*rgm<-T-(ci5GYeLo>V}2sHnpK4U-%0JH{Z z@>y|idThtwHVx&Wiu~;)1~!Dd`g!{48Y)|kl*rc5J|Vau>|%ZBNf@j9f}z2<|P|B2F&M9nt9DWle;f}H1mEsp=;-#PXQZ!yPUA#SO5+qPB?WzbZ3B8 z=0tq_y!eE#h%5UgyL8$`>mOF#TAC3A9^8TSdGF~n-aufUqwg{9LclS9y(B#2Ti7Oyo^T*$|jM z50&Zn<$-Q$f@TGEN5>~B$=^hx3*Jx?Ad4Y2X5aqv7osE6q{+1)!X+l>0V}Z|zCS*X z1Ayd6xqA;mJUs#v7PV=&rLw6L8XpxJCs0+UISu@!Gbrd>4K54ndR zo3?S)c5ASwTgg`QOF)c5)z=YCzvF3&lKngf#8`-Sk#IkMP8$%X}OW-f?#TQQWcvHl^&|%c|mqPhiDLzl7@CI`2cWiYsj>r^Ho`YTc}H zg=QkxNZL&4@g&IH1Sbks6GEh~g|*4L*ir-oq(g76ac1j;BT@-g_^UBFhj@;CX=s$#)P(CTVuKE_x*pjMWQ*SuuZ{SWk$UB8fhJgU`+ROt>h@MI%aU@6 zh;N`fZ+0vFGe0A>17uNmfk3y{;{G12wFtA@sm-5~m(@quh8Qe8i0>Ou<`InYHDy3f z09jmVL_d-1#7U?G^fz??qeB83eMAa=_-viCWfE(3aQja7k?C$RKE#*|)g6g_ zH+X%Vl`H~+ z!e70q-!_|^fc~YqKVa6duf{6>!_NNS(LCSg{(=c!V1dHs{@}8IRMqv<&p(`v|F*aB zNBjDJ+uQh0`p^H7pTBJn|9wyEf7Y|>|D@B#FD*L-^RI4H=zRDyRb79bh33O99RFLg zy#8SqjxVmW+)Y6f2`IV(&P#n?xeKZQGkAjBCs@ECeul=x3*(kwUp}Ug&+(m|JURV( zN@Ik3++PBkF!gfC02=7MRu{aYBj$pZ^%bR14_!G5yP|>xnEkZ?3f+T^O`y791Q+98 z0|!f`D|&nNs3)Q|>|rRGF52)qDj$vKBDIVvB)Wp@fad+>B8VnOJ-Q*K%)6F_LX<`2kHkbV zXk`lO2|(v>*U1S#t^?!~Tqm3gurb$tjXGj0&c7FRF{QZvp(d~pg|2sSSN4FCKv)|d# zKf0Z8@lezNd;aTMVJ3HrA}%n^?%7dL=Eugs>j3rmUiZC-f(?^~K`Fd$6@J;9?eDG=l)t_R!GPBZf7uv# zIZQwNX>b1Mng6;z{!gf0od16ShAtq(4jOxAEY=6jHzV z2OYwp!|nH)gm33h!KtT7owL)-J_-SVGZO&UI(=e}@FFd*?GFT!5f&!^gtP86oDitw z_}}DGfBtwlZ>bA7Wo1DWxAeGM>QZjO84mT~9=T~Rev6kYqT8ZP?n6NLl^9>4k!on! z>mQdV7Dx2h3xYd=uQ~#_MGaH%8i%S8y7mh?;b31knECV~d+L`^X63@_TINS*C}4!q zib8{Z@V<_X$h_#^bg7g~=go}sSy2x+tBb>lBs|2qR;JuJ0FX{oM zt|UtQd=vQWN_ubsl;Pj3@0V+Yv%&+B`Q={lMnI^4vpD$PS(MG-c_`p<__0300QXwO z`I{5`-3Tgo-zfIuMp6#G@_Qo)iU0Zfe%xo3!8M!!uK~ggZuDdGpv;5y@ju^Z6@S@q z;kS$XaSc#durLPR2&aZN*=WD2vo-Zp5#HX`LVu#OY&et>A$(xpOd_ACi6ddxqm?NU|QyH?)7IS4}z1w z-Sj^tdA~H@`*$S|MnVJr4j4o+Nx4!G&!u{++;tJNs$k;S$w;*`)1=ed;85N4Bv5y6 zG3N8dt7ZhU3E=@$GWq;}v*hG=2-dIcD*9*LHo!Ohc=kVbQy0Dd+ye2xtzPC2wLpXv z_@}XIzu4p#MZaQyt+U9|<-2i~8C~`NQMyhp7HZ0Wt~?w!ClX6YRs}WYPU^1YXQER( zzYzRx`YgeoxqGW^nkI26)bm3RrH-hVryRNj<`R)BqjgPkDuZ(u-kqFu?#8~EB8A{P zT++@te)jZ+XH~d$joiQ1qh8*~f1l98hN-LcMMr|l?%>_O#DPP9ph|#_IN`@!MIah* zuHq{GrD>{gf=yGw@5v73zfDuY-zskuxfNo**?E&8o`=}G$K6Q1?5PF;4|APFm{&>Q z{@D+)S$p_KyH}V=d^~P%6{Vy7T>{4r~ic}t?>LC3%-el0)1`2W~@=*Bkl>7ff7zWp-7|r(AfV^me>78H^Qy^lj`xe-w2=mpR32) ze}0EpilwRqP6)9P$w@gc2eeN>$a)F+^Awwd>$iP~C;amghsyOlSXKI-t^PY(hj8G0 zas-ZPxgN*~-QJv)G~DAo35cQ)GcnHhUj}L`ZEO z@btFZ=&Mp^rOD6t<}_cJVdI$@5LXCFty`fT;Lr*s39Ddq_I!q|W>15r>|{P%BdW?O z>5bs7RIlf-DzoUe3nxi6p|;tN)}{mQ>Xu_s#(>+8xVvuoCHDqx@DgVT-(@4C3Gd;_ z-VtY)#colz_UL1PnT@A~xKB9;3>$0cD~S{M%qHLq?z!T)3H{8v)5#C<=WAZh#y&F+ z-YRq6h-LnOt`8w_^AmiVI6G>0m5v(xcX4Is?A$Gt^^rRjUXAst*YAkR6+t? z--aWiEpqO1pw%d|OsbF8ZM%nPY)_3ERR^WNj@!Gcg1TV)z6#C-wIKzAL7$c7Rxv`* z4^$)rC*r(fGIO;<^yzp##_l+~a`v%rjAy5MzFHTW8&|zaP81`bt#54Nqf-m)r7T4- z?8qc0O#hRvh~?N^xTqun(L<3thr?;l{dV6nyl`k-9IwNkP#|gQVK_< z;^s(r#D5w}ik6k<6vP-N+jqm4<2TDPrn}{rGmQSLjVP|tCOaB}ht`XZ(&%Vj#hRjV zkN14lQ)wYC8_M0K(XI8(&2~g~m8pM+ukTplf~aGnI=St!S!=DdP1P8f-!0Ez8_gj_ zPWcO+*kVj_5$8fwEjCzgb|zOC3B`71x~H`q+V>)DL;vEv4wb?mv%gAnoR*(!`w2v# zU>{o;;IehOc&(C27HZm%29k)j$bC(s<%i|WOy&Y6NRs0{l4Z!oY{Hk#eAg}alUiZZ z%9bw)Ui;9W3x596e}+$TtB~dCBplE8iQO${n_`2lomdSj!jJ_eK;nKo!#e5N zU8`R@PACgZ?gwuc<`%7z^d?Jpbzf0AP&dSM6zJum(_Mlbsj*_TSn>{r3jpeajH? ze)EAf5Er!UzYg{rR}{(;vc)BnN0X0E-S&VTDsYV=w(WJAOoMPcPYBadj+lb^E7pUU z_7}K!=!gO{rs@d6dSoRz*R2EYR4Bpm(LRibrb>cn>3p~2{o{DEgngTJTqplb6)&o# zS|OrwqC5$i$0Z9QZOfM8DfJ?>cOlAG%27FmG0#`N!8O%<^AU7;v2TZ1&6CwWUaehq zu6-tjh;IQTLz+NWJ!S$YL))D3obJ-KiEH+4uJme zL3&_=9 z->CE|h5p_uq0s)p zh01?N6uv_H#**gu7d~t*r5EDPAQCKYzHNFXv0R#<6ql#e!zba9A<#|Dp8TRJ&)27K z2jg<`?2)vV_<83U!6m|fd~*LGo*yV6HZCf<2mkEh-1ALX+&bm_%vjPT8j!POg)qBM zLZ9X3oo=;rGAgN&J||=1W_T4nj#nVO0wgX?)kLb7k9Xbkt12WlgUf`~mW5qes zl`{c?t~vzQDgQ?B=$l`kU36j^wmXtJC9kyHEMIjYv?v+q?~BMc#Z;mb$Kc4(ad9D% z>xGjLi7K=L7`H$aZTitM_yGP*Kc2({QDv#r?T$4D@t{^`(VaX(Ck4EB@%Ew6m_QR3 z2hR@3xLXb9o>ApgA-f^((mIFmfWeqETQ|P?nm(WS9n^MOU*<>}VPpCR*|(2`PBY4> zRu-d)@|+^Q>enpUm@f_V{F1$sW$7Q|3&}5AXOjl=copyHglcRAyC^njId1YmZdzYX znJ@yS}-ax3o@f|fR(FdmSZT@g?fqdubv8b zM4U_<#@{I}^7rfe5k5{du^sywof0k7&;IcWjGOWql=R9E1?&V(B2qzknTF+fgRO`K zL8-dbzVn`3jM3uzt4cqYg{bqJxR0sQde!Z|pEO|=LY;mUocbxWXg#=T~)7XoY2pm(Nh6W_1SL^6sLg*^xw?v19fCiLu|LnppJY>lu*op|r$J~eb+4M^ou zRuVX`XiYegF&|7jk2_Pmw2olO{lh>rnfXYT7Mza!wN7Xnc5Mg3PZ^z|Bvzo>G-f?gx2sRiR_+dw_bpX`aCJfMgChhH98 zfmH^!0a((XmHgjXFi!a8)32rl-z}VhHCm;g%?k2&HuI;I!Xxu17W?x`q163}#s0if z7!~>xi~V_}--MYzzSy5v3SZoxSnSU$g^>79EcWM>!t(h)vDlwi`pv8SK=WWAOfdotyk0+)?V=`idG5y@xe0G@`of zC7;kqf(-h(5bC#1ia+uQ=U+8>yJLAmEov)|oVAHWpmiCA$H#c*bbZH>2fC5^8ljXW zpwm%EI&?bGoEcN7f;5Q6yE)I@n77oH6M-@0?7GdQahrP1wo`nMwX~H#!yV1XQn|Z^ z1L=T6!Cs0hhG=7U$lSS*!Zug$ELOd7?6Xd+;&%_qu0@4Z;`52#;QUSvX8s0W--XUf z}ZTg7ql{LAVRB`&NUw zEa3TBg-8Ivvdu~hWT}Lg@%rBEd*Wsb`MfADy!*RFb1lji?w~n#P7qZ=DbvpdeR>rl z@+#DRJ91Q>$xiv?TI^gSRyjF~L`gZUlTyh$9U2Zv#`M4kA4_nbIyDz#RfsmVqT=1knoz++O;T_R++xJg%-I-3; zoY8^>FEn{tbqNg@;x=^CgOd?yQTV-Msg84*;)8EW-@VO@Xm9MNG|!l042o=;8@5CK z4wQ@oMC>Z_uat67#l99*!<}4_tbO;kC`gZ-3Kk3&--_Q}~sxrZ^UiEmZ0s{5*0v9>`IDF7%a2oGt- z{(N}i>$mv>m;9|8=0_ScAgs`51=Q&`mbS{@SeoGcC6>1Tt0d_k@i>?#Ex50P>Wi^Q zN2Ji)4S)5Uff#5x?|8q7FI#*KwdafHPa}DxZ(XLfhBi(N!1)q2k$c~DMr)b|&^pV! ze1!w{uv`Vv8Npz~i&uhbd$a%VA1o3U@Ox^h{jdjz}SZk(Q zO5EC(zk;KX7pQ2Elzv5FDS)6QBv&Ly@+9$wYNHD-Ko(g|k0eXWExa4Nc9^F(3mdw) z4gb=q2vL^iimm~I+fsBhx%Qs9Gh~G*P=0OhADx zHrlGXc-vN5pU=tqG~jq-uwP_Eg{W+O?R)FiwnKSXe%->is14ISq;W;Pqo#=9s_X2= zHtI)_A<-Kqb1+3(PD2@x`aSZPv^c)^sqLDoUKQ3IsI>C&xSs)= zKmVAF3KN9~%0wBX`*O0h`e9|-y+MC`vP!l2gBqE$j?yQ%OFuT5$`eE?kRKllryn1~ zXaCzz)+Yt*ocWsz0V#VfWskn<=$lLjE)>pJ%SChu$l)h~^Lg zTuxYjeEjQLAvF5SgM526{q;h@_<2#BK%ahHD<{Cke0|h_J%WpoD?FD!HVOIda##76 zk6q;Fr}+Qa`>$Z)@S1|AXEAy{&~i{|CGKdt3X>jQsxY{@&KY?EfF^?(c0a3_AV6?*87^ z!W;JoyZd`v`)y?Y{oVb&t%ZS?KVcUB)7Jh}2>o{t?$^Wqv03;}TMMtupD+vmy0yFI z0SgSnyt$Mf{`fv>@UORS6rG`k=?r24mYLRI38 zs9DYi>yApfy(8aVA#F5Lnw31dJ5%k&_grbmiF-Vgg02E55!+uRe}V{~Yu^q!00=Mq zcpuPEf8W#%zA5v2e4HQ0#{9A24%qPedu$eL{BF39{`mF!^>JXs{qN;8|9L)O>EZWp z!oyns*znKyNd8#$=Oup}+P}Q&&r8BX`!`nodC70l$X{Re=Otku^xs(Z=OtmR@^7sA z^O8R-?O$H?=Ouqw+P}Q&&r3o!@i$iec}ZC5^fy-hc}e)*|Hi66F9`!~e`D33mxNyC zZ>;+Bl0VQz{dcbo^zVQ3vce^QsOuPi_uBjnjsE7ffzar0Ue@zZd6~a?S>cjDAo1(( zUK<#E_$#Zv0R!~n1;;h6BTxuV+N!N2FJ@fsuFTb#;l6vwz}t@K5}+ zrWZW%_9Jrg?<=JL+|U2;GC%rYAc5RHiE3fU=l2O@37+698S(eSKH`@O{rzI%zgK7F z{H*8+!@Q6{uF{u4^7gL?C^>wqk>;L)8fXk#%y4IOisWV(RxBpXGBMV5 zyF4DZb#9Y9@h&M>zecm=1*m_{sJ8&Qk`=H&14_ZF59C5t2#!+mfe7}LTZNqUX^{Yv zbY$ZBz~-7UHFaL~CxTh$_hpw-b<^BXkAv^T2b2KMC5+>`AlS&7T*?+C(e6-Xf9BqS$ylT69A)Vik=Y7_ zgbq7yJ&fCMsg1UY=LNT_?pR8B-95!<&lJ&j{y}JiwvmXXRT|eX)Q#rY$WhVfGUS!3 zZ$DZOk|YlUysoMSKzQ17yU5UaH8gm^;e*bdjN{Y+EV6)C+Xb z<#4cfRM8pMn9q~g&afDG6l6gcPH|HP;L589w}5{~ zWR9S%Z}9e2lZr^SBBKU5EwA~8oig7`C@it}7Utgp1xgFRnAo&hpZ5y8&7{`!z@aDG zvS>YX?wf|{idjP^HUbZDon{Z~EE9_Y7@sGSqs2Xu?huc0SUQrhWqyO%RG)ahgGnd>ppTlw@H?S`K)5*1vA>T}*viz+7~wHZtL%Jr%0tNGCZLtu@9>L$8@ydAwmUz!WHj;xIwP#6(j}rv#@Shz5nH z<{Xtx3g+Vwb9rLq)pCmc1a?>{tgaQk={@a}EN)Xs%zdI5z_RRFMh?Fuxg6sv3tjBbnxFJkMnbB77JjY2MkMt zPOrX}P)sF$(#1jliMhB=W?`}oU@nNP1n`Le_b9;sQvTU=dz!De3kT#=Z=V`f?vtL z|3dQ}hCcqB?7In=SAWk{2N5S=-0UUt6liI~l@$o?Wb0e^lrL)Y*?29O04~DUg|UbQ z{jKc#FErobjFmqr`|e9ul(;Vy_i`9Fs!48mieIe{5z4+_IHSQ5?w7Lfr>2(mPLjiS zOnMWn_GBAvhcLsN5G8*m5E_lLQ)yI87aJ*q2m2 zMK(n^UOT`A@t-qOe??jnbZ)|or22FlA0dm$x6eGiu5@Prb8*IxS=xE=T27bCy1FGb z(h+R_xMtP2B(-2Pt9cOY-SZH9xt)eu*qQh4!YHB6*A08-d_;xi=p*G8LQnPdLZOI) z%<$tUD#ymia#EjJR!+!yIy#TxX~x?u3I;>b4iD`kk{>r;ufYHD4PQt6xtrS4 zoxP8Rka7TeXDEL}4ge=hRnxe1>yfiM?8)$+V8P7b0Ql8T<6&cu^R{lXs~v<&Tz5_7 zeH_k0j*zWkZK#MSlZ>U6O;D5@```o!3(fV3FPCl{jzmhM3mnR{i^(|*UiD!c)^?vB zy`4W!BXPi{-vQw{l{FB1#y*9mz4MaI^Dptu zzj$7N>AZO$-gNY4eV_L8D>-@A6u%6my;+Sd1_p17_89?4k4hs-NdPgQN3vqLS0BZ& z?m}`ZGc*9GS6>7x2f_qA)LDcyDBh^cB|3L==q>RFzAWJxdDM7V-MtIX`*?wiL zx30YO*Dg+OBs5~fBPaQ3zbVHmf@0g@cfX$ycz?5^JYLai;oh<0^ zITEZ+)QgV2y{fyU?L`&LB0pfri@)=L?Er#0VDyF;U_U2-nBR{xY4tf`>y+{8)svwQ z=HYi)C{X%cF6+ew7<2xnGL5dU9e~-yumf*kxXt-Wk_>PLQplNzG{%V2m7?dsW&S8hMyeCbnFXyH0r9vpvtuji34;unr_#&zUj z^dJSN%aCn}3~)hvxzP(;#5g}&a1e}^n{w_qDCy;h?{i{emudgXjhp>!EThWj)clwzCMl%6)fULk_`}T1?y`O!;8(L z+J(VqAKB^XzQ4j7DH@ur@Gx@ndGNjNdM6V`5$PcL@2YtDp{gVXaC}2*y(9FF(RRzX(cyED-gmp*yt;j+ws+u(g!=l8qf0I!0#$S2mLoDDx=u{vI6-38@bHC zCZ2yB^O84gc`FNUv9DGKl_TKQWHth^s8Xcn4@bEh-S%#_qmv`;}o zJAb@&aUT2c zz%L)vI8Wo*pGANL?i2vP^yRjDuHGhLP}aH_-g}FC&_a3DLK#c>E)t}0+w)RNdJ*+@4lrEV!ANCR{I;r8bxie zmqJUB^CCZI-=k0Ohy9_uQ+FCBwdx$jO_u|MqpSPWisoA zXS$~fTqJS(xOc9V?1LqTQCZrRzT4kKYBCoGRZI%(g^(6XE~X<1SnIWjXzgq`^`6J$ zX43SuNJ#&rl_B*G)HEFZ`05@AyrE(iL8cEJ*&N2|8Gl(nX!2qu53!f3u^xsJ9{@O; zXQT;Lb~Z|D4Ffrc-~Dp=s6m@`I(2*`u&J2kMn{Dl41#N{g%X{yD44S%Gj}bW@`I{< zG%VDex?-F8oy;5Vq5oKziS3iTS8@!wpx|YXRFNxI2$@(k1BTb^PMQ=_HyBQG@6;i*BIej%!dWSV9<$U6<-N)?kD#7IGrEXEZQ!O~% z`u=hh*wC_ho4*;BNVTwxCcK@34z_Mm@Yq;hz$S%^awDt@nifP|svt(J7k8UynN0~g zM0&soTyKa&ZkAGXF)1V&%X14AW@O~nj+%6j2Jlq=0$$m&N*(JZ?mgERk*tq(qria- z?xVCx@8@$6hH1T1Ect*W^jYlrHKX4fZ-;>kTi>qdGXh9*QvknR8OVO3Zm?&1vzM4C z0z;Apq$e)-et|rWcC+Deb-Okag))puIB3*9bpmq50O7@oiKDaoRDiEkY$+At_l4K3 z%Q9b@<~z3FsZxe`tnE=>wy`MOHYAC3~oB+p=QBZf7l8KYv;Nx$E{albre zkEm|4`9ZF+?LA73dHh~a?N=t{&jvdGHDU~J+>fB~PauNjg>#Sc#y`W&yyMn49{l>h zAx`@fAp04O{iwV62O#?gUIcKC7k?2`MgW}q2DSf6p!E-b{%+mhZ*Ba;@cvJk_?7>m z&9zG3fHLp=2q;4$_A5B|n?mqEco?|)uNM2EEB}900{wp(uKRWL7@~_`v;zIQfbc8N zp^|9X4lk$L(ve~igQBP>PWxMA#yc!onT5_fu8X0*(zlYky}{F++rKWw1P6GdfX6d- zu@#7ZLAMY&_p64O7S39Zj6<;wYQ!}!ed@#`K<^|z4PoLO`XE)2J80_``XzNU+3n6F z{Yk}qm*C`GD7+{@gS zA*yW-y~~IQSFidtX6?+yb8x?cLILp8-^rO)+}Nn=i0=fVD!L%nar>&8=^MdN!0ZXb zvc9!O_A9?Q+OnCff1q+tRft3N^Bmzd(_F6BV3bSh8cS9-RP^vj-R0k}cNwr8@WS6R z?EE_lq_!u7N6Q|;f*g?nIC*oVk)ihv``FY%`DcCz?QBra&U`z^_h)mIFiv|ruEIv5 zf>}8V>uel=1T(5}T%2@N*WicW;U0xyN@|B(Na0t2SH-t%oSuvdNvOc(3GBoc&I~N@ z-H|hi^6G_H!0j8xiRKtIexY{$PpX-z4%gfi!J6lizE5TS zaJ@Yu9;Yx2z38kzGh!`E9ygJLL{^eo(D=lLxdjU+{FP*H25{~PVg|Sxnk$9$Z)f7j z;yfkCvpBr)a!D$u3o?o)?>ayXP_uG%Pv6-(i3_ln;cy=!`_vKfp~+&8ZtE?Njgz{o z;E=xqH$ZL~AEw&vv;r?w_NE}+Q5bYO7jvDY@k>ubk_Df5c#^0~IsLwzg;gl|yLJz_ z5uIQrwO}tJD%3k7L(RALM_r{4+vS+gI5H{MB3ru5Cc`aVo;pp69gAIDgSwa)beaqH zEW`N{9b!1}bI{rVZo!%lyvrhFNAJr79xg_Rgn6>#;W!=3pbfo!hz-q-<}~r(>}LQ( zK)b&NYa<%R!{YyysFEn5lXOEby)}!i^IQ)0V0ddgImT(+B<>B>HNYXOEwJ^}!3wBs zZvKw~IZnZM+#}OqByyXanJ_(WIT@2(Zi&+6&A&sH(h{k~50tr}jHA^ICHja3$Tbw} z*tBJ-c}ID^YGkf*fZ-U zGu{ZIYndi?1nn)~So_iIVH^?j_KBps`jBX`{6|RkEeYLF#B3&a zo-&$e)w!i;_kxuj=yx)&k2N00qxU=BTsdjhN6BYI4$DPyreyg69A~!HuUh}`?>D#t z*_kFc5->LQiozF1vN~EVl!Tn4Eh)l9c1m*-pC6AtjKQ05wsjvavDG2f5b5o1o`$%7 z#OL<8i%Ad*d=DH-h$>rWo@{xFLn@S(`)@n4)oPHWN`QQwU zPPxXs8QP2;!*iq~BfZ~v_YzAI`_lZA7U%Xhk{b0vCxEWsfGU*cS49mK!-Dv#PF%r( zMA(IqPAE5^F)^z|x;Q@L^o)sbW|LvmiD|g8;Q9!y-Z1z;nQ$nEULbObQf|X!SvViu zjO<04Ekttl(!yW~&1TdKvo#nTD0$FxN<=mKqw(P!q7wpm8WW_?6$4}(w!KDzonbz? zuVh<3MR51)&y$iLr}~Pn_2-3Brb$E1Mk*+%bIx1g^14HPas6H>!6L;TrS&4w&I9qe z2{Zyswmze?aHb`0T2=!&&^NsAO+i_OY{b_a(}MKpAUL81Cyo^olke zHLw>%f<>`!k~M%yj_4((wV~C@kO@6{8Ecy;uODQ1zY;I#Zc2THr>A^x-@Dp_}+obcP!ck1DE)l9T@d0*sVc&UmuqXU@rpp z-&M8&*$aP>-v#ndl_2yNr7$pd@h^ok|B1Q$>(4(5W`2*ku=3&{zbu2{En)r%M}AdO zjl5ug)4qEBlVvdf=giXji-WLW1RAdX*9T$!LyP@?c*V<~;Vl1&ut3emFK1zAL|N7V zQJpoMt@~6|%_~VXVdOaDow0A<_wy{^&Gpb(>EN5rRvg^{S$J)^xWw;wu@U+oT}8cw{}F*%pztjX-4JGIB>4 zbEl{RsxI-xz4ysE39hV3uvmU|Z zJOvYjZ~SGL1iu~oD5$%Lf6H;n<4nJ<@3uT0jj-X*9z3*|k2!Bf)GH~9Jf-5ry7=LJ zT=pR#L%G#IO2-aRY*T(6@|D1bg-IXmjn67KI_@YF^^7AItM7BK0abFRf|$LFZs??N zhjrR!ZXqBv+MQWLCLwYqu$W!O#{ z?&K7Z1HHe}IQqOV~2f@bf?c;qu`P5Ws5p_d(TDy)7^-kDNx#eF$Y0kP#S}x{soPMrxpeCw) zbsNpd{iI~3Gw+1-8Bui<5nGZ`u%&SeekwL?K61!qZ1WyDM93&*Tq8%yjcmVXpS-Zh zJZSo&US1!CjB`8%O?B$0y?D`k23HT%ifTfRDiO1r4#s20}f z%Ml-?#tSs#M%}WNhy}4Y-j(v5x$l9>_s5fhI?7d`nCQ&`PtLflEk+MW~>ATR1{7E_j+s5XBS=(Z&y z`t9<0lPyimvtcCB!y252O8)K@`HJtFYUO*U8r zKtDR%JLYcI)0z%;I;1~r*{FE6s#(K%$%z5z*MgxBS*t?{7D88Jw9?ql;7=kvJe$rU zb_*u`MVWUAknGIe^f$8C3!n@rsyf%Df{8Yom1yRBYzoC-om@(_RRG&o^v7-{&+AUN ziqnq$VTAha{C*$Pm46xQ8JW?NL&y!;M;2mQN7-}1sZa{@{AesEGr<=)H8}jt3ku9_ zZ7Er8UrXmH$Kd?fyu>K4aVM!6g+mV)DlGVBoeH(s64oWi>hLnE_PpZ3wo`?cRaFP4 zINbKYRdv4gqMfLVcEn?>C*juZc9~Pw;2rXW5sZ>^0SB_onwz zzV}&=1osXmmG(={SL5Q840|uY2Pu{hwar3Q{A3h}#(8;ei%F<@WZb0SW*7+5ij&h{ zsU23hxyMzYi~kxl`a(<|pih=>0ohxSZN!0b=2YXS1~ZobO`SIQ)fZ9!7wVvY5#s(4 zyJyjpA+Vl<{0N%!@;?i);T-Ir;U}+t>mXnnS4#gp1paG~_z!>nOSy^Piu`^1B4HOB zul|fev2P^4(p2F$-WJhhN2F!t&=?YM@O+HBG$y4W zhA{%*R-ch4fxHtVJwlQwJZgM%fl-)MeeRneVUgOtR?R4AmVQ&A5aFXIWUAfaIm^^ zi!p#GolWfPVmsJ0l!;H$BRC0ZsCc?-Q)aR*8N)p6opYyA=Lh*+p4Z@o=>ReN=|#AnU=VKQDC0A` z*a)r7hR(M_3eva7VcJ?yFr_t{sc)$FscecH6GWloxZWLaH*7ySx939Ctl)1UtiOZ! zR+4qUgvfu64^4qhc z*?LWYR{`^|XZ>8IMsPVv7eCt3A!5!wD!5As0C0*B!JS9K^guhYxq~gd=fLWobK{J9{pKS_1~yh)eoQU& zcUaJ$S*)4lbx4-U3Ae_Qz|u&54p;h-wo$Wp+4f{lSAua={Q~oeD(E zgBjVw&y)b{(6RQQatUp1SwW+KC=HEE`@|vhL7xY_| zQ!%AE&ptJl^R>%^RXDzCTF6ugeYpT=>o}6yBO)r;&nljj!ROIX^D)M}Ic=l4YqBdM-pDN>panjXgWk zvqaSta@dz6I}JdhgnwfY!bnu=d1>TJm7L`^6wl<)}}O4%38aTqbS`f|a_6C*|GUZSCH6^JdEZb9tf^8+FIqBEPF&WXifOv1C4? zV0*PwGd;RK%jCHB@{=)gSCm+n5~q4J5z%;M@2CJ}{G*_UGt`oUsytv*pSW<+nZQrU zNm(mi)9IW!4z>b#vrrZ;Q^#RjO6E79Hmfc3x zqo*+u&v(-}xouEbrRQ0V_+WVTn$`h-NzrnGT*grpWMeE%UxQ&TuuT@w`Yr~O#3}~3 z)nAi2D1fvsRFzu=aV_}#^Ns+KhBt0}VKjXMlz3*|zON5g(Y|_(CMdU}nUM_+_f>>~ z_&DOtYS?5^m&){-*X`}mFvvV8b^1K-{YTw2<3ZluS8vin^P#Uw(LH(jZl=pba)ZQm z3Xgw=+nA5tUrasgTHuUNPUPa}vufMkVU`P~u3Im{-Tn0diFoJ$i{}5|$GcYlo=C zkNL1|bR8B!s~Ue6pRApn27I}DL^f-ZDaiMsyoK&n(^KAz4f4Wo`znkyj;6qn!~4!= z#eB(Cl(!p1NCA!|)+cKM{~v|vi|$NI9trmHs4tUSRZpYc*Hslq9JvRO3ZW^E#v7ZW zvxoA4*-BJF5`9v0&e98c;uD5UXSHtvv~gbmz5W zL+}Nhw;(?Qztr^b)S1`upc2yALSQnr3Ygic5MO;$)$PZutzk&J<>I-TXx~VFtQz^{ z@av=J-lh+?j_8(VeD`IuL0e4(oQG&+)2!}$Qcz&0q&%EP@46QDB-h(^g#n=^77s1q z*RP?z<46f$Kgt-(rm}gVNZ?Fj3vx76HK9_p-B@i?F@ml~b;Hm5eMac#`Fy&;hmQ>f zp)z`D9#s%(&9pEVhq*7zP1^)kr#FPKMy+C806oeN=ig-3K{Urs5zK_x#$kqM0!V$R z?UR2dWb<6>k$L0u2e9OnkBCcSd?!2ybn(1* zu3qgccwTE>*K=E<)Aoug^je5Sx6&FFV+4UUewsR62-Eky*n#`@G49NrTHj;awv=`| zjTE&kM6zfBj*#6zGPhfX4lnjh^i?%0IdK6d1mby>GFqE*5&sl*sBIUPloj2`%{eQ*(|EkF096~&Pi7WbYvH(mVkY11V+HjgJ_z{Cn4KiIe5 zW*dNTB;#p}^XU^O&2{s;c)?hUxy^R@(Yf0^0N}7$Lr%JFn`Ut9%FNrP?z$Sh0BGMF zD0x)K4Mi3LN;w=JUGJ)L!MrlZSFNFg`EZ)Qe}tdf7)bzN1Nnthuugo%z+2 zt_ZH83igcHX76!Y8m6B!Sw_p#JM`hRttN1R#UJLLf z)x4jpf_C4$=58bGF0oofps_lQO;gjPFVPo8ITzz;3EGPvq} zkRMPGZXS-rOW*upI-M{xlY~` zz@5jh%Q4@W2a6*e_!=vFL7auh59$d#Z5^(-=BFQ`M~-BO$xCn~I{kJ5m$4cX7J(y7 z+I{kiX7Sns3y>RHqua*mV5ZKl4#$VtR4+6nkJ z>vNLh?tZ6^C^N=la{=y`fYFLgHe3tU^cSH;#_(B1#1iK3=$0)I*X8HtaSi2Zp40bw zn3vO~?t+cQb_Cibm#&v1MvR5YJ}p2+1da;*s%v$#jsp92(1#C@p&Sm8MXD3=r2BNp znKTJcz=aFxa%wNGw5?&Jkp*=HuX3dj-TJoMWIZLa0qs+5kA?1iL>xb*MK;IU z$E0}KJ-Tm`*YLt5Ga)B|*TgB0>>X{&HzS5Lh?&_epDff9`)sUFFKV2c-oOyz8U4Jr zWoNAkz(KblQ}#V*`%J5&`UI2?GH83I(YIDP?jox_iOjTjA-gIiAD0zGO#%srh<*Ibi1ad_?!mBdrD@LCNw=^eU{ ziC+y>gocGae|v#ptsuCi3PrM3zjc(wx^y-^%eMx$`^?lIGbqk#M0(q_^DjFToSkEX znALOZOaJ>#zye3^OEVg!%@{7tqRyvb#+5u|9Q9$px+-ps8!N}V&GDL#1?#gf6o7MJ z*k{qGy2JV{9Td8+gyDwIFB2hKwVS2o@+ODi;QDhOB9xbAAAI7Zrm`|%7rty_VXdSX z!ZA6qJ}PBnoKTGy$|pR;6Ps>aqE0-cYxPWNgVtGaa>k6nMYRd<~lzHGsNZhR5q3Lsx_=dWn8BC=hh{6U31g`tY%W)2(C zB=x}F%m>H)+nB3#4yaz+UZ#Co&jNdrYy~D47v!Q$PP{0r|fqdno;0Ugj5L${`K>ZC%k{1ZF52{*`8j|5@rf|G97oHn{z3 zvqO)7JFpkiyAXZq09?P3z)`I6*UhN`R$1hZ9G*Q9Kre%^^F4|i^Wm$St!5?o8ElwT zaukBrAlT}{PkiB#tHWwZitGnu)esh14@5v$?9w%qg6L6oAb2(0AN!TSH+l5V0V%hs zT0h0uSlcJr@^bzi9(0Ky6>5FzTByL@sQp$TXXH6k?GsBk=RwCq);ZR>n-lM9NRK_@ zn+9OBPuxbQsT6F8r~y`td&Y296>+p$f~lAcAshq)Td7s#W{g2=8CZ0^#{lZBVR+k_ zOJgf(tip%CrPCC3aXIKp28f#vz6$gfe6;55Jy!~_;XPL+!@I9(a4&B2tC+1N%j5VU zzDgsSW_;$s2-L8Cq@jMl?^md~i1;XPRR^HN!HRC9Dk~ z9Wz`^g~`=iJhN@^qKN$_?qM1GsAw2UB%)_4y)N+ebl=p*%!Z4G8qE--$dq+$hF*my*$jZ4f zzE5ENN>X|GxqbR+%<%Km^E~SrF>U)=A)@}or|3SY{2Odmh!XZfL{4R}DP+nFH#8f` zhL%>c-Uhy`nh(U>3>?mK)6_vCR*;S7dt1Ee*SBz!%J*e@$fcZduVrOeDB@wDsBmil zJ-Z+h=3?nuL8TN9H(=CAZLjStD5jufJHNd)8+Xm*MfZr5>2K;vjn9QIRt!xsG)@tx z*m~r$lp2+UWb>`J=_rKtlg36`>m9s1GK1~0+y)%C(X`o%H7`6f_Z$vklm%df){&j2lDs_&M=BaoE00D5&6m7O#BLqA|`&#k>^LmF%08aseACmjsm$e=hLELosOUHVI7tU3*h!Q*9#2 z(;fk#tL$*7x2z|oH8q%#drH&=ol8_;xC!tSibc$*^5A=Cy-?f2sYKp1H_VKt#dKs% z7pe|L|8mr}8_eZL=(4dQcwHAL960hPdPfnVfT>Iyk<2RGyA+!l|lNaLuB(Mu2E`5Y^|!v&kJg_cHU=j%!g%&tNA{$pGzYa;AN_d z4s+!W)UkkiFR=f8qi$Fq+esGmRSs2 zEVVFvFRaTtgx#a!2M2^eKZ>i9&JWVe@1^z}wTI_5Cu>7;!!Gt0kG#)l&jH|+7E||C z78=2G3$VB@K+Ldm^Ye*`HX);(dzi6dJDZ7Zt70Ao_Xi^xX8;7^(tBOgM{Ge4iwOae zgKKIPlv*ez#TSCM8PpXq^BTPKDp}rI{Y3GzC^3FYh6whYN6#`WZiaE2bGc(aaO{$# z=_iHPb|2YIDxM19=n9lropPpJ6S_=~xsX90E3gsHbPpwfE;9Fu&WrAz-<5Bseu&HK zTj8M=>@!X*lu+gy%4xxb@(kCHFo3ua@Oq)O2G2Z+=|o>HFfcc#beh%ld;;!G5xr)A zox*3cUQl{dxHl4@r9&l9E~wTR9Z&$60f${ufP4=avyB7xe32KV$#oJxt7p6lDdHW+ z`m+n_QfF89qmzOuowm9ZJLH(YGZ&x))fLC=AiWjxAZQQ83WtJ_9pdbiCVPedpXIAN zx@oMn<$57&HBI1;4HEEdKW5{8B_@`e#crrdy;kcadb}j1x4kPb&=Rs&$etot%$h}| z0mMgni1B$EFlL45ORc0)8+L%Ro|4^oapfuPyQr9-2l-P)pA! zY5}k#h;5+S;@Ts++Mz^1y)pX0lQT7;PSq1m7R|UNJ64%{I>0km*1ldSTNHV|=&f(u z#uw38LWF+n(D;OFRE4VM%4pPrJ2%VyWaixDyEo08QA7Taa}#;B1e9C+jBh`4MQ zgfAFtiU4$(Xt3LaGVb(TMDD!I%97oyCy}b`PPAQfBKju#5$cR~U^&EwYkL4#)&top z1T$GALuL@{{lX4{>_PtgIBrJw@T z@rA3$3d@fUmC+QXNXuppGRl{R5dIGQR*gL+*iTUob zfQ<{{1~3u_oUy@>&U)?fCkGplU0h|S5n-Q8=E=`kfFF1F{koFs%XOdnSI_BU=(_aSm@bV?g|F?;$qQ^a@zRqBD{KC| z=7aJd6`=pEmHu{(1o+S0Oaa~V95rkSvNllp%0>zINT?{sm67W&^m}Pb z(esNY<(SjsM8jMp7rG9O8)enTuIQ<&c~$8#A{dPqLx?%u#E7+F(&by)4;v z_^g+E0F#MOM=5-tsRNi3cj2|o!6LNM(Q2a_fOnkX{@B#hHv{?VT43JQ#CVDy{^||T zP`hocJbVg46C3!~b;7f6WwTq<(yOSLKow`MT}(thQ{cu2&)Q7!Tk{HeflIz47n9_C zBG-{x*mG{r@{Gz$Ifkhdar$R^o|(z4?bA$dII+8zz1NDqUclBw3_SBBV=a}oSteyc zgO9GKA)K%F{WOLr+$Nh`*>EpZa^Et3mxH6*P4_KyvohQBP|ADA|Aw3NiZDQKK^rp|Fq0Xgv=ObIb5}M-0xK zxS*cK##b_ZnQz`+TPOY`2^k>*ZjF909wO=qb)(a@n3!Vr&{-%XVA~--xg%(5Bs;D} zTvR1A>mWMg>Xbg7E|;6z*53I$`0uQr00q)Xxqe#ReG(mPpB)V?{hOmI9P60UrVaA} z3m-XlMSBhGNG+x<5&>8MSEwTEx+}A zC7_BEG%szX5k0KFVjbCSJw$9`5Y?mN+bQm9DJ5=vZ1DPa6&N3! z=@p$tbl$>ftzl(^8m$lP*uLGkvA>?i%e-mS_%Vr-&$xqeJ>M%+H!FGe_TFJmQmg4B zbKuTKQaa|ZuccYSLWX@(eW+Cfb!W(1U_ zLJgeW>7|O^4hZNLd7HiNa?8%o^Tj>pRM?2|HZPh}J~=jsqB1<#Yf?B=JKZ;!{eL8p zmaTJf-Q@F??LO+IeqU+LxJi;p*QNAM&dt*Z6(TAt&qacwMgLKs0ayQ%B^v*z&j3bh z1mDKazu#STDswx%f`#f?iN@nxx3mw7_<2%1TryaU*;Ykpq1%#`W6BXu<3Umta76|G zZR+&tL3tJ1nJs98O7U|XWU0q3ulv~CQ8K!?X)fEgs*6yzf0~thQ7xN8rR=v1+ei?j z5Y;#FoN2c(mzmkO-aE~w;Aqa?)^+z-wIzJqfulgM0XZ17iSyysjIXlAi%VSb6=OfF*_QoG$2Y(^I@o#%0|D*iHUmVf$ zllI3y^fw?pFu&!T+&?8azWN*dFNL7#|Iq#Te*l~K!v;ventsgi<87EL2iy6R1_!L_ z{K{~2dW7H+x(Z(z4*r~O5#UsR^e}MiezDjejidRWJf7y?R(1Z~I2zF*c$5f%TVs== zX4nisG&Y>Y3IL2346xU4ZnB(dJTBd!vKWiC?07v9AAmK3Y}I*S+scOHXcPeSvZquO z;9oN;fCWar8m%?VU)IZ(Wyvs92u5%l_nJ;3K}4u~0!07O01Gxlhjlv8*ktuYVWE8Q z6Lzcs<1INWQ`s#d0i!Tmw+1_D_jU8ZLv_gQ;+Hy3USy3u&$GsltF&&^6TN$^1kD9l zS-2xQC*uqE9=8+K;$hBwICpZSo!qbDm11k@7#me7G6boPk08Du=QBbtkp34?6wyyn zUO)4w+E5>Q8T=%pBsR^Z7pF{C1YFKD8;6%=)enf+a2@e0t;}8Se*o0*?sOooS9uPx<{S+b6v5an#)e{yI8}F0fA<0MRGP z%DBS1^zbO=7!it!8@dxrePotMRJ2pf`t+%zZ(8R$xUQ*dd=}O0M~Q&J<%mm59noR2 z`g}!W9E}lHOe$7c2)ZDOImF0K$w>u=J8x2BI|sx1aUMdRwHOZ+>mhTbs`6kMKeigE{jdHdn5i5NT9jA<-Whq|c zNq{tNVuee5`*qMZDmSiE9n=HZST(!4&9PP;T-xGI-C*!oa&8kKi>7@DI0U+Ei|RznI}IB!8n9(Ihe4L zwFtn`qx_|K#1on~au4$&{= zfe>ki&M7)^xyTe$4cOZo^BgaWv4x4X1g17b)wtgT?RgnilE{@VR@!ZTTwa1C^96!U zJJg%Ok;5*XL-J?e(5Cx1YU~@yA%iZVtEUR-pb(T*RFo*8GdkPPaeP*YJYxPZV28$7 zRv!7O-aoH$exDN4sC)>_+Q!iYPGHZdAaOH5w&*+;lEl4zipXWtymAV*rBAZ(85^DB zf>EYBlVtax6JW%Q+;{)*FWcqb>IX}#{CR1dG7Sr&a|Dtpdd78{gX_n2P7wPwUlwfU zQq0a74^rUgCrwTOgPG~R-*1l08cf?-rA~MO?}UZc1Kw~(rvk<=-H0;jo|VU2u30<+ zz08ztL;j}y^ioWte9`xyF%TDUxJ$`X($+R*8(=5^p{P+f1hXLP)T1xJM)9v^V}(d0 z;{mXSG64ZFpby*K6soA0ms6@(H=`1`laUxbj$?n*NP6C7G`lo*bcd0X_vcRkLf@1bp}a_z4Qp5u>b-l(2b; z9T*>6R`4cJfaA24q%2m8%n+EnYoX5M3twsZb%bdO6DxzhQrpaKSmQ2%NcZJ)!5{1c z>Yo+F!^#o9LKLmiuA>jY7`~*}nqYlZKBDjJypNeg&;EWH+Axv?imiCn-U2<+j)gh{ z0|It@^H^{*WDa0>jbi3IDn^@x`QH(2eKZ?kNLPLBQMYNjch6aS-V9UG=~GbO?(a?8 zDZ8P-qk~gvp-shJZfHy|O9k(HiilQ#3&8s0#a3GPQZ_?2hNB7$p1Cw0^YHQ;Hmrwm zi}Z7;4hZ2=&{T4i@nb{_@OvDBA8cTbK^m>+S}m^QdF%R*qqN-A-lE=n8yuG7%6ZnA z2NLT%6;A=Qv^qK~xdZIj8%{iMbK4;@w2Vh`eYP07Irj!3nbE6W_?V&gXb5s^F3R~t zFdV!xdXY%$b=x>_^O&o~ocoPH6lVke%>n;S_|y$GdZ^f6UzWJc))M0$^@(-Cc|rn? z7WQMg;5!rFChS(fR*8Q+Bsev|UJ$V?m>bd{fPzWyV%;W3B%X+n1@nlkEd1^pJxE#7 z=b!B`9{X_$b@6h3o9eZpvSYS*4V>a06%Hy)282YBgG-W%3MYdqppUz76!Fn9vCG^< z*J~BbmJTHY2!G9DyrHhBl)87b{~Br)mAVPTy5q5kTP?Y! zp*(Z#LQPB^MH8_pUQI86!)4)&**?x?_Q4J81GBXhU9 z`%pM$hwGG*{whXI>xm)Mcu}6`+Nx&0ZQ6(Jmm&a0+(fPw)xbKZcL?wdCb4&?a)4(M zYD6{@Q;3OqNj9kM!_?TvRkWvN3RSyL{nJ*S;TayNxC?0CQNS@cb4|yyjN~wQ?8M zMm2O35Nv_2-K2m?3OzzO>G*nt%vJ9em7na(IRnu4=y5JV;EP*I<|~ioG(GUBFNX=q zEk#s(WHn9)=sA0Y^L!s4E~&nf!Fjm;T8$M|Hd+F^GS|n4HrD#3+?9GE3M$4A?p+t$ zyWfIWQ5P$ECm`~AS+>imxCIr+{Ag=t?<-97CM92Rb$oZ8_u)UBMNx97* z%=6|lwa2?S{GR@T@@nK%E}J^#kQcLpYU4HasS>#qwfoLr@Vm<#{>JW~!l8N%G9ELg1F?GdH2}1&wp$3}CZFz0g@g=O6_B3I>@iwBAx{NNGRu#lW($suL|9 zYtv6<4x0R~q9SSU#(g@3LPSgMg))zb5rb~jzI7L@fr_2en3Qax)&pu#&6!tELz1-fpa%*LOe?wzqVE|Tf$CkLa{zNyr!)@f@DfGxTZRv}(`#+fTt(_t-n-jT|> zR4{fCGESf~o^dzGB$a7fE$&*}%q{zRdx12)ZWy0Giiukza?Np*n%~Sevp*Vx?p!wz@~gtG~zTE+OtP+YhYZb8wr7#ZrsMJz{a_bBC2elrIDc`x}vPG zXl+y{rXMx4BTIGDP}u& zI2Tc{tS_t~W8MlGACZE-gE`(m&`r!qO=hoC9JW{8z5sO4EgdOmTn-!n;1Q=WS~*}Y z6ce~BufdjWu#$oRmU#Q>&*yyjeh2J4ZgMcc59aYv!+3JowWf`r0jz+lNs2*Ca2nf% zdLR-fv)2Q5oz~FKOXSUcb4V6`NI)c#@MdFJ;zcPH-^05hPu0zmMO(E+z7QP7(;Aa& zjtw}K>}`Li+e}fB}wE zgxP_FEub@>S|d-YvlX(}9npez@wN*|)HF8paZOx3}M7n9}QuSBkfb z9NYLDa&Qa<_JYdK>6jmM^(L#v9Y3cISoIbb1NYNwFw{G@9~aM$x`N3ZK)oJ?3iV+VQX24eb)g#kX>TDm7j`wHmy7k)`Zyef?Q`3DxUZwQtVhG2bS0lr-dvBdFGos& zbC^=wAxQbs$ zP4n_U3v2$NLcNMG)(uSNzs9=%?gaLqqUjKvfGzr^bir?9-MlZ-V%YqOy}qAH64aJ&GpbCWW`BTt_F^RgB(x3X=sKjQ~=^v_Zv z_xj!DuTaKG^X^7aUB@RcywKJGeyMp*+{=?%Jc4w$=buwq?7I-)?2&R_DusQZ^};4p zA^Y_3rT@2>$umY7Z`XQ9j^~w(9vi0! z>HTtswPb80sIBRZcxgZ*)Ew%S6dj)*eXnL!AgjhqI!gM!arh#a!_#84a=!{@VU z38tCMDe;bsXg?Wu7^BXD(7h6;Fevx;_3Fe9BHqYS+i(~|1Q&RVUgRdQcG6LITs(n4 z*JN%;3`C2=)P`lGOA;nf$Z~&e?h}@{te(i{(p8`|WYg{m>;bD&1lN!qjgut<6yVCL z>sKZvah1%=@Ax$BeoVAHe_>M>iA!E-NuF;{NQ_n-_%52Z7O>SOwCgZh=_NL`Z=(~% zun`%SAxuEm%8x0)%wK_*yAxs0FQqf>VirQRV5KZXb4_>God)3t?zDl zp|#`RDb8Ht@toVlLdv!{&zD^=n!XWboE>t-Asp~}SPvSwX9`;jqw&j31wWJd`eCXYxmoI))SI({wU;*5*F%4wPCGAw#kZdfwTV(`Ax7L2-82e8fK#TA z1eKR{cx9)zg}-;QVKVn}le}n>)`piK*uDM2!a-Qw<_VKS030Q8YG(o!NPj1EvUlf@qU0G1E(m#BrRB zQYKD)Hy9Hj>in*WGa&S+uxv5F8L+xfwE7A{dqgnZ`O02hkpbuzzn0d+N)qm|dw@Bu zF!s#|s7wgti$|{vMQ9oW;X&UN4(i$|*aOZoaU>w?2DYI`th^v%NUi~GS$!`?AjdbRlXEoNb8OR>9AzJ(3%2tR3 zzxxx>W}%;!+GQ_H?FX+}0WaNJ9=^%Fdv+%il@gWc+!;Z@rt61PL)gc*u~$_5@!nUz zX4OK>PppgbKDE=M0A+>nh5JlabfbICdaJK%b4cp`BtzRgo`R@^5e)Bdx9Q+DhsF!( ziNg?8LGXoycNBrYPZ+G}4SGodA14ysXtKn4t9uySkU;p3-s@go!1Mg0lvE+wQ))mH zXnJf^kYF{URZMe24%$}Ys*%Re1w7UC>66%|R`_u$Pn^@*8$RP|DnN^EsrVd1lOwtf zXpEt;HfL$W+OVXH%nH6-U%ceoJ_Th8qyXGoz z0xFWLk-}(@sFGDdJ;IqW-(S7Ts`1mvzkP;PFztDir)&F=ng>A|8#%Hsa0y1c%?+rE zX`^w?+K7||E#^d71RvfdyY5a`vS*ku*EHL8)8EFj+@|#|YmTNAZ;mw!83F#jiKsMJ zPz9|=*C@V4BNL4e^O#4^r~jsRBG?>x+(Ty6-?rM^J5Rl7`iZv+zGt!pnow#*6CRCy z{M*plQ4ibN)gA;^>s3MN(&bSB~C>JZVuDH@2()n3~ppKQo-UV>Vyg(bjq%a zrOhugVG<`zyo+N5VP_K_9%$n0IIE`eIB#Dzfqx~6Q3qAE07nTeyP&-h9Rorhl!G%= zSgy>nr zEY^WNFQrR;ux34887CAp4DVau0H5(w7U@D&mvfpMVdCnC&g_@hy-9}jE>z28LOr(( z0Tw1Kh^-)kLM9q$F4W9lW{tJiNXKoioK8a;ZbozEqsy+}s-tOgIHQ_s(S$)`*km=m zp`6qin4TY1o%$TJ8hx;yb?sTO`6uqvcAY~EOir>)#tzcOtT;*#Ph!V6l9ibVULpJP zRT3)**@V;UHHVVvZd7`zY}i|v_YvD2`SV6%x3^=Sv1NY)w65;n`6XV)R!I;Ta0B)Q z<@EyC2esIM>m%-#&uWNfdH8-{(gxG4oplFj;85a_`ix3rJ{EZt&lmXOR3qD!b+B{k z2AEqQQQ%KB2UVfsL1jrn@vKF6)blfHTNg*cCJkM0<@=SOiLXNJ=ql@__On4dq13MxoR>L8XzpyU8gB?!v*-^}28aB;Y}HzNgjfG#|^)snV@B1ij_A zY&}z{5tJ7=U2;YV@Od~J)Q3%tfUG1n8kevn)w*KVY~eM5k)dUEixE>MLOg@h!%L6P z**UqMD|*Xu-i0u-@@FyEgacq@*`qK<{W2&K+5w{d2k-mnq2RPZ+mZaBk0sxJ)TGNK^_ZDC7mPpT4JV z$s|OR$<1R0^0XI@k}lEHja=Zq`LYUXgVaJukrdh$~`5BbFT|j z&j!FK6veE=F2q|w)rI&>H?7FELJCMK#SjJu{%%69F1#}2vnROaUWTu@+^HOp#{Mx% z;%W7Sh>1|qp+ZDK%Yn5m2~n2v_YKjL|`v5NfgAOCabs%R~ML*NtC{L|7ea3zN2-&>SHYb{p~6?^6!d&{rcx0B=kQ^mHzvp=wHoD{5LaY z{_F$5BC-F1l-PfxxbCmj$538@>SHYbEA_G81SR0k|AI>-z(%voPU>U;6NlV||t-P)USb=RuX$r|9IN z8WUI-G?vjcWEFIli-a1bHD>SkjY`;Ho9Gv`b-_tgkUc5)mQ(nv=QO9A=NpHy^BG{w zyiF-s->(sZ8SHJAH!MO(j+Tf`Qxph|7#UEd3v4&>?I*Zh`cwdbbEYmuhR*8N;3kV+ zKFSf%0lr5KxgFFUD(4^Jw@?agJTCHQ2~+X*!+bJ`+3icL^YnCR;`?=qYen^7a>N3z z1vuN8DJCrHOLT>+zY6BsI-9@-xt+Pb&*>O&CbLt02Bd8JY@eR{`O;kvWr|ZwQi=o> z)IK+71BLwkdgqPPlsGHk8{{-PTg@4Jc)(_pu_&q8OU)n8nc*5h zE#b#|#Ev!@@43aHw$KlWS8m*isSv`a|3C@!aunT3Z;v?e| zc0}(8B9gVk^$piMYsG4X(w&k7rX^v_D#iSRFg+^;U}uS$E~`4vLX?iTfg5vDVWh+V z#Y*7NMz5j>tF`})kEk6)%!=&=A+cPnPk*$Ctifqe5~w^nt4ijD#yq=voLlAls3i*n^sA z^-+)pQJ|RO19}PL^H{*%{eo2_lrYl*P9#2mo|?w(_6htmRmP(UIJA0U)bO1QNn#Gg zrdLafpfK3j%uFtl7lXE_F(ZEvHVxV`%v5Bm&19npGTv?pcDGf z@cIu40+#=?!+C$K{!Qt+N>bCt#N?hU^hCRmfDiPE)xYz@PQVKC`O;7|DkG=G zsTuYL6tgk5!~9!{_Ap8(qVE2pv(#@vAYilUWfZ;0e53JzF-l8=R4P_JCK zoM&0FFcIkq>6zAzDk0|J0rPRK=iT?lGT2XWdrS3#>pbkZ=FcJE{3Uu<5i6WgC87+I z3fLljK|!Sp^Y+(o6L(d1c3eF|@cDC$oLItgKj{mr_d6ZHV%&6RP+p}_qJ23;Y}xj*15zP)ac`?u@rsdsMveve^9#C`7)cc&4EMbN zMh#9L&Wz!!H`;~sd;Y>67xGZ9SJ`)TH#oYfEj7%FiEJ3~UKo&mHPUc~xe0Xw_c*C^ ziURl|2;5SDp-YWd3Z>sz^xk47MHNcEBNssRWCx%%2bJe>I475H-9<%k;c=PY?QxVB z%0KL|wJAV1iIM%dEt+;e%+M-Hi?nHg>uMM9CID=L-<|CdMMHo5hPQtFhQDa=e}=Uw z--3^yevLg27C?0YHPFh1s0)7Fkqgv4oGn1vUw??@S7rrgo#lV}HUIMmzyI?pK~wqt z%_@C=18YRSqAfrD3+|`KEcSxB0sH^khxnGD!?_^`SvOk>j+LA^7FDw`eZ;gh1j}J$ zB)4hl)j$`|o8Ape^Ugt_R7U0cj|=-c^%UHi5^d4-_2V}v(macLL@DRwuxHEx4_h|zN?cy9E5pV&`JbXs#%!pa^!ZQ zJiB805p1k-iYC-@L=*d?URbLA%qP#9ifuh~m-HKfcO0~?kVB$mMEpDpVg}!w*qt95 zZgClVyO|*d5hgcjJYD$1JQuNi2iE6|u5r!k=qwi68ABvE2+AS17Y+dIl#bBH8C5=4n!byC`*?p4x5g_z|ft^`S*+IsCJv6)K#wy1F&t z$%Wbjyv-}!OsWEgs-*~rVA;PtV$i?;d}4obmjB&}{mp^?ehS|AU!Ub)9K3&B?YB#P zkIkQ7?YB$8TH?R3+HaSFHD-TdwcjoUAO0___S>bt^W*;fYQMG%7ieu5yI9|dwlID+ zzi-Y8ZXPif^`MP;7u9>+7E>{kC)NjV!4%AC6*0-hgXKcmu}=5ryi--H;joW zXd`X!kSM@ue<81=`f$$Feb`U25;!>XjHNMRSc1a}Gh)p`1*qJw{(Axs=`4+MCphvQ z_K1;6XU|S+wr{Iykt40YK8?;dkh{6DyYeEw;yi9 zFjn9?>Wy^-wrtYfON4vvVR(qI(|SqQ_C57-&2v0~^^Y;&vMV}m#e@?is(m$Hj77YK z4)ZJt5#6hix+53hk~Ry$`+;QKus^53!J?{{1CFX*l18?BdfC$yx``)G1rc)pz?ft= z`CWevkT-2lb%p71$m_a&z2?-5XP*_qJF7m!q#}uETJ=?g);Q8-tm5PbND8)#Uj(Nl zxq;!PC|Vkh$}x4tPzpHMkIPByMLg#FOSe3{eZbU)1*`aQ{^9bc#$Z&TTle`pl8nEt zXySqkII?ocin*w@Et{geAcxdsTIu5gvqIc_ceidtnBXR)C%u_&jS%!?iRuXlFlmXC zT>ptEK35BYWSpqBah&lA2_H1bBD2&X1Im2+=CPC6hcNWPh`j^lB}ubo<0h68^LIwD z{dyGIFG()c+53h+VXF~DOeR>(_+_y7&y7O+Ka9uyGzx7M|FtSO_}6-37&(Eeh~IkR zzw!{$cM#hDykg*=eqQ?G+noofAIkS!uleZgY#!^cVSwr%n#6Vq^)l~sK{NJ4G&>^Z zP~uylC-911_3~}6YbpdISqWyuBF|xNi0RZ%0;@w;WR-egY4nq??2~8?eLc3%Dhmj^ zY8K!sUwFVz5%=l25z5XQ8LzE8qGdRGXmi%cXf4uZikyiFX~UD>%9NHF2%fVj{{s@= zA^nY6b&gO#?`~;Au|@90y^L>E!+KtkGN%E#?5^maU0Gwsv!n}atOEO^{zxJ#D>{FQ zIVqr|E$^?BQ#P!_j15{DsEl1(ygpSaVjq)zeV+W)Q*T_*-*DzT&{;tVV4!-|oKqCW zY504QEOo(NEftRMN;&zsn_B&Wo*rT=Y(3tBluNl=DGvCVzZYPJh=mh zS;g_yYZ&iy^70B$|2C^#%<_LXVixTTg8RrGv6q6F7Jg;T+`d02?luWJ(j{|R776T+ zQ{(m0@|4{AckSs~>#7MsIhvdcU0-J+$HwM!jK(({^49dFTs_*-6TBdaxUhV%_0sj+^eHGlEEs2jS0+Tz#G5GSVfEEL zg)*gJ9Cda1xh1YUX(cBB??5-lGgQ`s zASmPVjr9L{4hF?8h$dP$;g)~D&RU&G^Y zXO(WNa=YEvd@dZQ`hBc9RMvxTVp)mlBohbSC#3rJv=2%kr><`dl!@&UDPKHt`b)Di zCF($_vx1xpM82Z;4LN__0#|)9Q|%kv;Rb&rVvVZr->^?KgO?ChDI}Vu$=Zb|_;;kq zwd_2{0_Hs3a7sQt)0JM4FAZ=y?;AsJtXRNIsI%_n{TWduVr3E*-R;rP*>-^aF$>iO zNO#uT#MIPR0D}?~&(r*k&#$P;e76o2QSMc&{v+p?dU#>^n=Z`m6+55E8mxwY&O^mw zkCJoO6$#3{Ezy1+itU2f@Wr8mGv3l@t=PgC52{7^kLQ|&?bMojg-?pyyE&27y^`(#Fq8ROF%m89aY)Mrt}6jreS?a0BKTl6a*)dhpyusjzGMOWG@N_HABemz$))bTBg zvJrD3Vh3vb9L132)>0;;>qps2Y$myK@`uKk^=Rc+8)BD@*6+_d^R$XVh)zQ>lf~pC zx&xV1ZDnv8q*KN3G}jh6y)3ZK^6GD19&m5gjz;QZ0sdrJOZSvx1Q{<9hXX)fqhGC$ zB!+|Zz(-P4)tD@5L?W~F7W^4Pr9v#H$1D zil)_9RB*Yzw%1~XkR({?g;e@~#q69gyo3}q}Ar@@g!C_=RIxlgp# zX@6C-ds=K9^{5gGBO+rRmN8SL>nBHW`QBGLteKuUwxBCrUWDi&;;MPg>p5e6N3~OBf>g!W`Yg0)&(-(0( zyX)00apX}n1y?pVhj(h5M$XYUk-F4$MES@$Zcg3tV$xxeo`d{gjPX`&qSR97H$9+& zS|MMcdImVRSRI61zTJ8JB6lNe76eg;4@U|0bM% zLa1MG^3SnvEdP&;)t@Z9_`R9Pf8uB8BL54y%pa-De_wd9QaOULpjz8UNTDYTJB;4&Ik|aM*)&5Ff+HXO&_Mdm5r;(1*)Tg2?-CnST6$-*YOAR@g2TGt+ipBGTh4lqhgu3g;DV5&SQ7^BClF`XR`GQgs;0C&R902)))@EBS=#$iHCptMZ zGB}q!Y~5bn<!#c)IZ&AJ2RSg4ot| zJBJzJym)7%ANbr>*YHm9C&Mb*iaPK#SP-#^w(8?l4t4zuY07=&@2H%7Yn%4&KBsGL zSbn8vsY6i883oGBbUiYO<1;=KHWl4OY%&q zZ}R4+A?`7aBc0XgqyRH1*nxN5(RVjKtHh*wEa$!jvANEu(v355)u7BL{<;5{FnW(Q zw+v#>e(7)D<#cRZ(ZT9pVMVk-H4y!EBfgQ7bk%k0b-#Dwt3H4DH(=s0CzV*dMtyZ# z7v|z@3>r6xqBW?y5cl*+u`c?Zt+Vy1H@y4mnI7hw#^n8@wI7o|nX|YG;Z-uk-h3mD z<#SK8iYV7#1@_{ITIoVWEsoR!%U6uzGl6Sotb&t>6NdP!^>xnfz*3DiYSJJRkPFIK zc=vLu=2oaP{vtk6Ut6<|eT-pzQ-$xR5E2vO`~A5}IuoZ!_7ZFf8^;Baqwf%$i1D=p zF|)_y5Sb&)2f9N?daedbQU#{HDD}NT&KC8Ikn;ubB@AJo=HWY2ba)@KDAW06 zKVq6Xh_?p}vWYe*vi~p_lF!xtX0QfI^o2AjCu+pdYayQrvN_W@eNFmDWR35nR?^d^ zznXx_W@3H4oF_CF+R?@%w^Q(?i}q019EDYQ8yAS5$@-?DD7?ujc};#`B!J zA3pV%hoPOvo+*K`1C)C^BC?5^Vtb)6IM9`VU>cP=RY85#Hq<@!?j z*A1+V>z5P6V?lC8ZVpj6#Nm`3f&nJ@O(v%J|ChZ#+jbO5(uUy+;=q|C;E+Lp5JCtL zAkIL{AcOz`;_82QcI?WbA~GU&SNGTNzk01&l^LmU+;Ggz9-9>h8g+8}@>mGOriL`# zD6vi89rpK=c5YRz)T5N1TDsegzn0PZ392Uk&utI>09D^gY!TD`waWGv9Gw5lGFr`l zm?h$V8$-e$TM<|mPM{nxN@s;JbDs!d6fW^R zQU!%TK}D|7|K+>~AGknJwvV{dgPHA1;eB$gSIw)W=D%Gw8A;Ma;D)=UB)siJ4Z#g^ zdD)dUCF&qMO#H0+4vM%(bt3lz$r9B9q^9*33rq#|4cIL6@C*^Kfv9FJr|l`n{{S{S zFfG3N1_4`UG&|&YsBN(!&2;RoK^RBgqI5Cyk&a5GfAa;sj$Fh{a&)yfMHR%GhA8z@d{89i22{#0$PA2UjG;Ez)jAXdvDY@;_WoZRCp~6hk|GI& z4r=MAmlhzCF_XnthdKvm*|d#!FU7GVl=aaU&LwKH{9NRv`clj~AO%2C7Xns(5BwrPV?F_1>#Z54_r~j%oF}^t>1*H;!?9sK@ zpo0gME!+b*bASN38XnA9Yae`m&2Id1ct6LuX>ME*g3Dnnpu~O1tjgJ#t}-X3r<&YP zRr_Bzc<Y$#=r+JZ#1jqjqd6lVdGUt?0{pk$ClV#ZKHN24Z+Tt~G z!pNMZ))8^SBNG$2y+)|av`=5%K@~ZdZ0=)?2zEwp(7A%)X4W-Kc+);o7Y>orcH~_f zSZfN`(@pr>NecO-UA9EPna^2lqm*8GmWFCNHd_r{7Bp$Sq`yrN`=fqkt^NN|U;m%~83oY)3yqBs@n6aW{%CZBEUC}f zY2s0NngTr~m(7I&RL;csg)EFlj$T8C`pUHZdaI0Xkcb*M}*A z{C=cx&`4!q=!$~kYN#^tAmRm=>QF4vA*P#Uj!`))nHlv25Dw(Zoib(`W0U1{jnZiY zwJSzZox?pg4$6F}_hs|!cJJ$E7rC?nrk3GQdP0Z;yrDVlV4+#}Aikk-OvgkCEUl9l zkz(Zy@|@gqgHWQka85?`hkF=A>U1V#)PIRb6S~_gQG)s3! z9V3q=-!wtBM{a8zzJiVhn)*HyqmHEvLk}TlW&(R+x(1tKv0}k2#AgFYx3$?OpHqjM zs+deS#anDPGY5OOteBFw*u<7pT&7V7e71<9^0I+1U>hi|o3QUqg^UqQF*VD0@|-NE z)^FaL@8-`Z(Ars>+NGJso(yQ$s`GBNg00o=5+*z;m~&so?eSA zwyO>vC3k*;1pm}OsAQWM?<-_ntXV~>V=!$)h;1;8T;PFM@;*>vf?=; zp4&W))pmKHAZ$S3B0?dJq z@Vp9?V@oaD7{bVjd1}IY&lJl(zSf$l%!-vfXJNcQTlxf|H?ao7I)}c3VNTG|ZIC6N zH_^NEeVQj`IwJNZb~@3Qaif{SUO(;BBgD0F$LZmwoJ;z^?$ez)G4WdLGpb(c-N(>K z_2E9~q#V#bC%U&CUp^Iek6aPFvi7JCx_jf1eW)krZGQC&6jaiMo5!xVd@_2#8HWO0Q_ii|NTbS-wf}BtA5| z^d2Mc&9f-G_jgQu^xgM=G&kgxf2&#jTfr*J|2b~*6}I{1V&0$r`R8d`KZu*KJn_+k z9bVAC)U1B1^ubW*1j@UAjGO#OLH}aw!~bVemF~8!?MnOI?=0S4b|{MgxqTYEqh*vv zE1Wypg#dS_r&2^~VUPeNrL*znnf54!_k=2Lw;d&KpQBP7((!&oT1GbM>9K*T6^Q4D zrcDCXq4Dd0ho;{Slc>z}7xpyfdE_^9jO{ONcv8n{Nv=a%!c)rfk73 zgXrCsxO;~Zn4S+1jpUH3s9q#fvp2m#ImhtUBMmF4b!h_q1jhcQF7xZG1QOxD7aU`t zFg~YzN}mDmp9MNX{fI+I3R&`^OJ68t$H|ksHO$+|tLr@@Q>POtoCOwMA5+Xt@;#Eh z$sIr#v4KQi5(fu-Jx{MCiO92-DB+ISg4?Bln|rGY(z~+zLh@pI&CL% zw3a^U=50UT{TdbTm!o-m%WZH<&Po)T^jVnw!TnD)8ihLl*kocRsq7is8#`9p%1^aO zXFau$hK<@&eYSmrxGnvz$n)aTGp~6ooJGgqctC<0xN9I&H1tT0%-NiGeGr+DRfGXDDG-Na<{FfC3q2x{pGPhB-Ou* zK~|FO%yVqRNe2i_=ts(4iIiPC#EwEbM+LysDQ9EmoG3rKWj!67 zudg_tKIQ+O5o~_d%mpTxsHUaK>rqVS0NS==A`wi=skLb1Fcz1J}1nE?mQS!5R)>eE2Z$WixTmUuBTPtISXIMat2;ALBX;%2&6I zLBN6Nf{Bnd<2T3~yL(5|LQ{kI_M5!(a171mI5ZUD8JL#6B((ec?eUxVV#=v6r$?VT z`^?^-P$o##p%CllF%Ra>c*<}w*LKLmK?JAE!Fif%wvSq0YUwGxc{I(FJK-+o`aryZ zp@T+=JTl`#m0{h`$TnBj-CKfwOt;D3K)O-$md`gvomzW?p42~Pjlbie;+Q<0y< z26+p~r12^Oe4D+w{ny}484ccyc%I)tt$hHdg91OYlnNX{mv>A_866qm`4htoP{wI} z2v6dx@>*?DU-5Z=OeVFt-LDINe4=sP(q=>Jo@j4gt(@oi<;8q!Z~H7YeaG_rt-6m3 zgJ~&LDhnNxW^r)nFzuO8O;d%Mx3{Nmo_Qa~_>87}c`bMekNXp2<%DY8ozCZ{v`@hx zrGviATM9LxJU7E2#QCAg(y84OZTsZa+wR3Q2o{D{H23vZV&YXQMtp}U%>-X8KwiueNBd=;_|Mp;yo}Z6SUM9n!7G6-( zlCv0h55*(HD!6l7&RlQY*WFIvjmLHD49FG~6is0#Fe`w*W+0fxG76hy&VfP|2UWL1 z6`6s~byvT-gLU8BFO2=z?%k|mTbDkiuuBg71K78jv4di32D z--&{c2*c_8w6f!cDx=zq`)A2x572j@$1w$vL7Kl!$!%^%rxuZ?Iq6yxYJ~SUF+Y>X zI|yxyiu6dKDeFE8Ins2Vfg!>2@J#r;GNt&@w0onr#^EZ8Cdm&$jqBLL&?oG0j-9o;;C?}N(#`yw*?3Ho~nlZqqP_5AJ8R$y4qPqhD*H5 z-pK$l!Y{Rq`01a2)Ve=~U+H)Q)*{} zo1Y&Q8ZWlHp{Z&*lN>O5O(mI!k$eEMHc2PC>t~b1Q?W&5@-=Emb$z5Nl|Ogy`Ou!^ z(}OH>N15Noa_G}XVK>k?w0t@SJBm($+)Z4oh(q#`MN zbRtdC%-Fv?&u43Y>dgH{d!Jqip`hd5e1X?x`LnIjPEUM(Rv+%HP-i$ z?W@;g8;xFQP7WPZ$&7od0Il8`;kp;x`*csoXZl(y7t7NLLjVJyDUWY;LId|CuBIcr zkm@LT=bq+8ipW%w{zAZv>Ft@R2r8)hMH7&<%P4VslSDfm&XqF`)*|?{``0|`<64Yqd&(;%|acgdOeC4p(Jo>>SF~@ z_Am0Nc;T6~rt{NAk^HMGFT}|{^R}5sT74-6<;oh{g}$(6BADc03dSo)cmCuOcLi(U zadf5Ct>dKg@hVzx*L-ilYrdb^Kv+nW*Q!2Sx#N2X5*|y#TIY;?v%_gy2KN4Qyy;xC^}_a0KYc0(U2+B65Yhm z77Ky*6FX^(PTw&x>6b7p&54*>`OUn^%Qj?q@G7$iU2n$1Af;bN5WIoL$^~W;;JI!6=*DZ1_#Q z+qzta|8Laqzc|I{@A`h7f&S}LjJ`9~2&(+cS0`Tl&$~WeG3#PF5Xa0x#CP3n(F9T8 zxNhrOj1yGqw9#ahAWY8r*&&r%Gx!@K++b8o&)D(Q`F_@L`S>x3+n?p}>K~7w@Gn&| zu_rQ0sZ!H6(Xxx<`}WwME))l`HGX5QL`$VHmn^ucN~0S`*aRNgoouiil{jcv(c#3r zZP~q~7XpjZ!skZD1x3)s1TBDT#R`5id&E;1k&0}H7i}^6#9ZO`Ra?Dq-ef(4n6iX< zQ5fzJLG5(i5e(giONf~!NsTH;v6t9GF{yO98p5C^Go;!9)Fm(3p}oZhN$R(gCa0ah zJtPW{)O?(xN8Lf2e7|+Wt=<|awDEe2c>lNz^uFeHB~%J&hO$H}+~+a~Qf{ddxluH? z7-g{Mu9?AYdT9?+QtA_OGUs&MlZ-yVO*e%wk9>LM)L>rxt%mJV0nMJ29>zd>>@GCI zPTe0_7{aZZroAJ25^Wz(yPV-~n=#oWjL-k%0`Yjr1 z68lFc-=iu5{lxSM|9pyeNQ3~wswxB>>PLM!Vn<_Pg*4}qH!?z-No7SpH0Y6dlGmqk z8y}vewL`ZbsRyxtFFXx4;VnC3S6*YySRxt}{oVsbM9VR#^WRH7q?t;KqY8!cgLvoX z5IlFf>=Vkij2PVBU0wT_vOR_)kGh9TD&OsKlzx9UWcILCuh4{&;#zKBb?6Hp&i;pe zP!#R_+!tjgoO!_;e==L=_r%rrbsRFV2MJBEGyoHs(*(lHfVyXmBAAcoL^Nfo+*}D{ z$Eg$9P`ljZPwqu7@iN;p_mL30O%Fl_6Or3_WK6SE>A;%v!Fe%f>QuiH@xJ!A$=NNr zkFecwTNov{1y9~=PsLbEToKWf4OEJ|L*_jshHSDBflJ!UzEH`o0*%lJ|6j3=*~ zUMJ__whfQt;eMho!S#@wt1RY|_?1Ah0Va`HJtxA@aRs$WcTnD;%)$GRV*Z=Dv~-J; ze7xCdSIT}ItEztA0)6VBGI`<`KH=8@I%L8y!Ni8KPlf84xkDE@Atv&fAX;{0%8Pz( zvLYeV4Z)iV8=M;k!mIR&9gx4SoUy56wX~GGE4FX)BHUsMHEYBDNDTCyHT%fhvJA%E ztjEPmP1e%9b6GC@>44yYl7r9%VFm)_UP1}gLoTDrW-@kDS+EJFJmO8^oaE@z9fdgG zD4S`g2I(n+kO4uX%i+MHf`*QD(|lpCqqI{?oxUb$CawD5=1F7hv(ZONTs>83J_&?H zxH-jl=*EfvOX?#3Io|)hI>~kSzm`h=PMzc*o>Blf|6gAJ|DDp|5Fo;wIa)=HZdL8V$U9JL| z*KC+*d#aCj+5V)2qSo6^oxIHPne@5{A*l?Mu^cC#J(&_W(;b_J8{}}q)Il8tjD0cN zc)zdBAYLlIPu>t;&}d&~a6!~ynm;duvSIh2*P*syUZ%n$hudvh5`DN?j06Shfa`me zw4I&Qgm#HlQ8A1YnqEcX%WTrwFcua@3iLdVrL$J+6esLNyxviI?zA$i^<;L${$XB@ zav43ZSqLoM9uD|;9!>+N;`Wl`*wo{@o4gLQo;z9E6`XACc-K|0Pz&-YG?Ff;#4N2w z6twl}91Lx8iYR)-_E6KmRhwJ927%J;eC6DHxN>(^B4(v488+AwSnh(+DI5c}^^lHSDgt zgpg=Z?3^gng9A_Zd+*)fAJqQjdZ)cm+)-|A?FD_Sp!QPmEA<%avSOAEy?~vOlKYg5 zB7B0Hz4E|Z(^>Jxy)3jb^H@j+3$(j;FJ!x&kY)Seji=Pw?ovUO3ZgJ(CVPre|G0_m z^B^=ItcUODW6yTCx3e61woHM6aDySd#swEd{f&c@{K&@I*S?W9NffGQ8d{(SzMWdol7U4;=6jpHn z0oB>Z^()u}wQSoCVIRxUrzF;dMroezmPw;iY@g;xT=>gyU@g)ZQ*yZl?29g?+?(;> zMqlGXl<)`7DF4~W$agU9KQ`U^QO7M5&ih+U+Vc=9$|kP{rZ9qQ>mE*JcI*z*bcTpb zJUyNsQXt#0a4}S|BDHc(Hw~tvj=V*f(mH0|v6$$E7jA<+v}>5>yeY*BQ1U@y<|257 zn`K%sN=KfPEhdsLubDO=La9BpGPcxCZsBK~)z>J!9>-Izx35qrPPTcU8@=0|4!|jT zeMZf+X`eMP!;oIkTq%If_};J|@nbxPesypr`;jd3OKidJ!aG~3!}8=1tAn{S^9FHU zl)8gH;=5stz3S;qj@3Ng_TKJz>|umNdKWs1*-`TuFQ5yl=Uvq*H>Ne2T{nUdDiH8t_J*CTf+Z3f?v zIb&m|{kv0{7n?iN)3z1(T}1dX%64D8tzL06QIx8k`Z$O2e|}cxe+|$7LmlAiT;duP ze9JE2jT^#sB7}v%?{*N@&I{6b%Yuv&0m`i^z@^YY@&Lyn-;PQadGekYi+?D1m*6=7kV+oQHPlAsP)q0Yac0DE*}Tw@ zs}ew~;AU?nY=)vuRf^+Wr1MNa%W%PmqZ;}{bSu-NMX1toO%lUYIVM;GU}EBDiZrV- z>zHH}rw~BIG)rG8?^O-$@VaeCkI3uCl$ll_ZezkiB`0i!M)sg{>Dq~S-@f{Q=vX>XqS*?8A`2n9`qB)GRHz;{RBr;@X#JYGu$+zt4OUqyrQs z2TU^s=9w_x22X@^ZuC){RR1L6f2XmEB|KpUE-vD402O{%6xR zj*MsQacUM?nR}dia!V!6wifirKkDVdo|eO;|!nB>ChkNBe*WC zOG);&X0_O_YCLZpmYW}^whx%_ERFe{8c2U}FELb92wjYC4o!xmXBt|f*v@C5JOmFc z4;zTvk9(JX-EJCJj5fS%g`IvV!M#5)+Vc+%9R62Km#=J*z>?%%$$g-L8DdHa?))OSyI)t!)6k)fNa?Z zcir5L)bgiwQfHK7QZv5^Yvc)A7GJk*?-99u{MN z9~%>a6Abh%5G1a3Y+^8r=pn+d9(4syB)CaN7v@~nj(mF>B%hqi{K2xd9rEVmU5v;K ze6kcbEL=eqHwO$#Dux6o{X=pXj+y#2?*{&qK=<%wBKO9gp#?BFKv22Xv=Q476747Z zJ#)tfg7TA^+GESM4%LTcM8_CzX>6m%JK^UCkLBC{73Fo`R6W{V`_tqP@VzpEzYPre zP1lDB{2v7${^x@WqmrU2lp3_a(3MFC3GjLfC46zmaZw}m^JE_b{YB4YHasT0Z%pCv zJfAlFrlSKuhp5fpY?9r`tsP>68!H3$0QDQsEi;&St)}~uf3=Siym2&OBipEyx3A9} zRT{iXwP<8?%kI(1XBM48UZscRpqtYssEm!hjQ8Z$5uPaUc)uigc(f?GqmL2Sj$9{k z&$vVr=@HYa^bXMHa!PNhj&>gDSIPFa)quLadY;NIAh;HRgWs&C-Ht4psh2eiyH5pV zh!uRgi|F33H|*7fwyK->-nd(ot);4vf620@Y2w1RGy`o_#$q`E=C_ZI#Ry#Dh;v!*3nZTBgHKMJzV4{hv$YNVYbK4F5d|s0+VdB|RrpiZhX0=w`>-Ym9Zb4I{U%_TFqSH9Q}o&Fh+^xZB=C~kLoV74-60LM z>fET8KuyLQ5;Fxf`5b!Q=Ik{kqd_!_>#Hf2DP)^`^!F{w=H+fR*WUN+JJ?WfEAbc= z!#p({hXd67tdX+UC1vFu7qGhdt>?jeIz7E4tFIN7rl+T;s%h+j)yvuy3E2AA zHG4i=d?jf6x%x><{dd=6>w~5IJQv+mWmt?&k4z5iUTCQNts)t*BtQ$7-$k}=#!_Bs zZPS-=A+UJ%yJ5Dq3g6DdXQ7dTR4##3qNmuF7M8n#{7-Tb0 z`Q?8gvSSWYd?i0vvu8%nD{OXNrm5;I35&a@cnKRMlGAqW< z+0vT5eNLw($M9>JR-nG1y?**KF^R1~E#r9AcirR;L4us*9#MR7ts*($L-9pV1!p*+ zOmPp3Rb@`xtvO_V5{hQ~+p9o+)O3j<&IpV<%r}(CQKc&K+SBjd&glgyLrEauAog3u zly-?y_tb*jcC8FvITn(`#2aoihCL5;7b#30zni z4+jrIAs>Bsu1YCxZA}I)S(}Tg%9xj@O5I7S5l^T1P2~>{Y`QH=9!a%7$Q?aZ`9e|+ zy%o>S4=D75*z7ts|A|oE`HF1J!Q~;x`O6`X9TZi`uPIa|O``yg9e63B6*_YRGr4uA zwfTB=pYpSEkIiXfZTW&yGy#U%ibC+<2Y`Uz+m+z$8XX~L~CB&achk!w_WwViSt^X ziGzp_5*SxM10fbhpAz0tEFxwW?dH~eUjA@m@_YA%mmu_Ldy6eQD9aXlIyCe*hVwjz z3(ags$H}hu`xVmo#t^A7Xo<3FbOZVoy@NYXcoG&RA$h3e5rnC@E}F+xHEZ&-q@!dd zmV5_~MG<0#fs)bK=={mk3j{*N3NE}WJ;krF=Am5njtPVLvtxyxf=)bKG%glt=YcF|4N66CpPBII@R#!cVX_)mIA;YcIhd1%-gTKo~d z8Bj|Hv72eV#(xsoW-n1+_d1<4CA8GBZZ8S2lcTpzNSMRntfNF$U0K!gY#A{4$2{`0 zCo}N*Avnz6@4L;O%2Z>!^9PRPQHduUUY2X3wA=`|oMfpsz7L({h7d)co7nFHI6f$K zv@nX}-e`-4jLP7I;LE?^>gKarCXj6ChD&~98xz$*Kvk1fS{L3)UD#f(Hfwe}bd+U= z4lIR2Od2xNQYT?|BiB%stO1Bz)c&T?AZM~}x8)iI2ZH5;fZydzDyb{yL>W6*LBdgRu8nvh^)H0{fEDXeQJqngVcD_%HQcI3w zf43e`Q@spzc})SM7?e%jG*~>1w1+Ix!SBC4HD}?N%N&jF)@3ONaejU+mHQo&T!}1< z3}Zq0n7ux*r2p9@ry0wJlxXyVfT#BQ#OL``jr%gO_ceRfN<1jve+5 z!Y7zeKBZZBJN^e)gL{M?sE4I=y#D(Hb=BMdG5!_|@Z~*6aN*oyxCB_wX-GplfQNX%pMKSJ=raRMb;hUeC|i7%Q3^^KyQ1%m`c9 zSat7=G?gghHv=%=5AImUKpIy4c2WE$TO? z@gn}W4|=2WUjp;F804BS|80xJ*Z*yR3c5bYTjD;l)-!m)ty^sN!nnb#0ebM;U>4*G z!bPdvq6w!{e557xC9~5wFdq*1=5xfl!vnv0ynCCRDxj}>D%i#6d@f4IW4HK&RYY`H z;WU-%#9jGq_NW;(7rCTjNRGEq)L-RfHgw?bBHxQo5ySVN*OBxu{dB>vNZ2oX+Fzl4 z#l_}?4Mja|7}eUDRX#S~Ln*%B1g$fKFB>XNwm_q&=5Gq#*In(6dQ}-) zqW(0whW*M+~!! z2lMP|d4g9Tyh>#W!YC$rTdM8ePIV?Pt#eZcyIzdH{LB75GWz<&D>l!5X}u~ey#45v zw9J*nwbj>+@Of=x)&KRDo%k>Nd$}8*k+So=7*e16-z-3+`X5^qzW&1<(*B1-@?T(L z6u^j> zgk_$gZrFOmHl3}h4Vi-Y3Qv5%`u|G##~*H&=q?z?H0k?6`DQ2e@h%%3Yn)&EJ+HX3 zkA0Qo}g$~w)jP4 zQ_tdMPd!Y%#5b1zSRR@OVY1H_yI$Nw3E@tIF+4yeI>TQIEDWZx7BycnSJwH-qk^8O ze{Q2XBG1yY-{yt`menCmXUfA59BK;~5XW=}3vvz|NZ^iSO$g;(OBqYxjY-lcCLlev zOUTIyD!=%kr(FP5bcNT5wZ)KU2$6LPsbr52o0z^U3&_|=>Z%TpSCz%fGqGc`vXWIYJbC4MTlma=TD(t0 zX?{T;+%MjVU4KFT4?29gM$ZbSPySf^>KOiTb;NDw?fp-8mURRFQ><=~roD>auVW#f zVBg2QR=9YV`ukuv|2=yCPy22^|4-jK2zbq}hn&9BSSR(Kw~AbUiJNUc7b8-PLYKT=M;)XJG8z8p=f{^rQpA?E;u7C?;VGsq+Lq? zi?wZV>@1L$v=~p~MWPFW&edn>N=&WTX^$8y5339wlL2B^S;Y1-4;DtJ)*`#Ieoo^} z>07d8I2M?AsPH(x)N&e^@|4^pis@Yh)i$-kz?uuY2}9ngP0ZrNpE#9LD-4{ z*{?}oA`p?Ed_$8LJzZ!8KNGDyAfM_}j)>jj3q+0u=lS!9OvC;<*Pv{;_7sx8P2zf2 ziFz)Wv!XuJGX@WwkMq+cQm3;0VV5k2+(%C(&=k&|Lqf6#N%7 z^oM=V@9leha=CpjkG<%Rcitv*v=xvy4+*8x!p7j#CuFL(-&4N!N4+Zp*e@<^Flm@$ z%YiE*G)*zdAB#Nl`yEV7AMc^UQ=Kb*zBktzY1rNGTzW5H_hjGp;CoBS!PZoOQXN zC>>_;3(WB=j7`Js#_{+A^*ei?;e+;nh?ZBL|KRG5x1iaKp#Aam@9OwNl?GWh2xRkj z4lFQF7U|w-%9XX9HSnVZwfMMYMw^}O8uwX>#ZB4STm@jrRoewt7ET*H&5h@+@=L}m z?@pagjLlU3+hT+msG%+yRw$6dvqbfUAn;chbOnjGrJJ*GfC?_t>T(l*VQkZ@gi~TAPh5gZh#TSev4N^_>)M1 zwxmRAg$3^LXuFjoj?!`rjl2tdm21uNXQeH{gYd2qaY0ZzVxAGwgYX29h`YnyI?~N2 z(Cy34bK18mU91gzV=O=i1PL{79?TTcV9=ZDmhZa-8S=&rQT+KXgxx!BlN(y+viSB* z$8WJz`T)Q6_nWs_jVps+0p_jjJC9qcS20Y$Zrazk}U=$Z$;hP_=Vj5|A3XoLnLUl4%J=t}-iP#XH4=62X z{Ecm!QSwR;fO#9LO;0sN6>ANR63IfR2{`CXst?MrgO=#>s#9AL|oJ? zEN*4hdnqaIu_4GCbKGEB2>^3KgAN05o;;yxus#N%F!5QE;%1$Ke)p~sBworOpq>|P z+T-^%5^jF!f?rVBnETK5;Olzi)68y5u})xu=kKYb*_U5(xKSX@bl*73yVuiB)PmctLh^lvl^D z9~OUET~nR?kpQak`k@&rPyW71HR#KXwfvI)B?|d*X2d&Zm!e$ii;s5GiY6@gASl5Y zQ7M>P3Hg-O6AJ8g05>c%uUR9a%Mj#y`&X|XrYVr~Z zv1pmVzZaO^zfT8NSlVDZbjD7mcNZvCU;m*@+o)*wNN`a(CE^J^yN^{0&Yz}@f>Xm;`jfb2sVHcKI%+?=-{^-2x`m>MV#x`X% zGN&G9(R9minvwHtjkN{QD60KDGgyZBk0sJ+0-}f@T<4PFMxzw9XD(u@J1;!F>GgbP zVvTqD@kPAc41aFOK@+$^P&kH>5pHppU)k)(XSyDcTD|rC?s9e3MAWdp?qpR@Zxx+o zbppe+f|(89w^7maWm=iiXUfAuQH02?G(u5wE&f*OXOm5Z>%ia&t`dTWIdnskv+IEc z+VN@TY278=l$D}_(avYO#01ZCI-9zP=UjWO^0>NE3@ zmo~ea)5DYAfAq6v;(mxK)8IhS)%xF7wl%)HjJA(dD15~t(fHPsOcaQsBP%oJVIiW6 zCct%-&BA!dEvh9G+x3cA&Az3Yt5Xm6VlIK_TDL%ZWy>wxGZ=b~e-srRKDqsMYCMZg zCmV!4&H{dyvF$*%p*(cOnACN*htauOk$=B9G@P;_$6Vuxr5J#gFbNPCGnCLWd4Q(~ zi}8mc*gjsn*f24`E z>6Cb{6kJ@a#G0OLi;HwyHA0Qjd#t!r2=%;rHL#|PucP)zVT`85^ZTD?)siRNKQB01 zy|Jc&XGmz_h&|BRM0g7IZw2+6p09tXaCbGVr<&v0JKh3-6R6TDtpdtHe+ue;Em)Q(#!yQVK*A&AR_Yal6?Cl#e zs;r~}!few_S*gE+SCRQja7kN-2n>#|n%AWihn$(Pm>+xKySI%rQjOJh(tQToE*Z9+ zx5Ctp_QJ-?iqeC;gg{TNWrNo?bR~(dih=coRAYt~&t<130sM*5YFw^Ph#@F9eoy{> z@@uf3A;y=DcP-)0K^J5GRzUWIYv;KuW@`RO{ww}$svk#9c+3?hs!3caF%*_ati)QF zss-r5v_}2{1X^2Ko4e|Y2NHCOwIgraz%mGRxkPz)co*Ke#xFw(8crshufs*{ODPsf zMzi?QDGIi-u!%pv)pg@+)3$6KmYEGL1zpG;``0gIyXl}S?ZrRRDiq}Szbh(w1ynz> znzMJn_Og$86b(McC|P{F%c_DqUD=@p{U9A8InrmAqk=SXB0D`ZcP3XwOj(O6G=9@N zYdI{%@MmXD4LhlE9sNvOk^`?GpVnkq((+C^OhEEDh=OKNr8%9z;M?@U-dxjsS@Wll zcHLs699IM3`Rf3(`v@fh2|>$WwbcrZY{&TAL*B{}698{BM+r?Y%MRRF%W=mv+S;?# zq?wW(4!p?6N%iw%LZ>te%_v(o;C(5Qg1UrEUfsq|@LaBaX6dlKExTj99+1OO+5Ik) z%Fnr~+G)ed;t4g#Nth+ZIj&0=9p!l4-JI?sc~X^nO6zz@Jo%`rB(p5OG%i?z$c9G> zU`HRPiL5B_k68MeH?Vi~FJOKjbMrU%YgVPDs7D{8;l15T;DENtyI>J8O0?&D$NuPw zOwGh2<`7{H!o1&5bG0UvY6QYRM+3ZYZPlaJ8ujU7HWEA#TDO-Gk6~vOGuo5_8Y&iEc z!hb1Z*=^71ay$0EaJXza+J^GEPyL4(13DRKMFt8pcV-b`^j4^G~4=2R`hj6k1 z086lSdbEAtq*bZHu|U0V7Pqn`0c76 z^|sRtR%SIOr9L0}?k_=W>uUvpy1YI^F0W8+#SDJX2q9H;nFwrOysIGS zlndxn*xLnR)n$CYF~!%ZNPF=5hc0AEXQ@cMWWh5+9_?u*p9^cps z2w%|h;rGZpqGRa5U*XOr?M=a4WVqLpNxj~MiTVk*ztTFZ(*zhqou)lTd=)nqgb^-Q z7$erV(BRZsb4J<)L<$^xgmIt(6&j;=oWVcccfPdL9yalg*LU?2J~dsc5+Y1tof9&! zrumy>f-ScHNIR8!HiTW=rCz^&_h^Dux@C zntKhv4t9Q6J$4(PT~@%6;bgDT0p3dJ_LRUCho&6jPSAEk|D#ACYUZFjKF3xp3yeId*##=Z@?e4de zgI9`2^$dS++St7|_e`lUwhmq!VIhAdqhT?Kb?xh!t$61_cHZt${2}0Xs`Aqllk+Ue z*KdC6rp+Id4;ua&vwq2liB3XSzwOWqE~yu~e>EUJTpa@< zC9O-tM7P8&ldZPM3kZWL6%-^!r7%YfnO_AEhfMhM4^M~rxyDYr`^S<#J9`4M7KhvT z-eCnKAM`~2%(_KD11f9KNvzxflm!;RYRLJJ)4(3TQ=_D?7n6*}>{vjF1YhR~#Ep1h zGCtJWY0u?l4Cf)Ro6+YQP+p>?ik-*mXud5FXX(9xF~gC29a1&A!{-m@ywq+c8e8Uz z4iUcW09C%&&yMaK%hz%dDrHn*b*4?kH0~BArq;hjOKVtN zqf;KxiL$I5Q*E%XeiYHEjk1_pf8c+RPPxTFe~g??-imGi3@G%e8g?O1BhLLahBrk58M`Zww!Q?)tYw!~x~ zU2@9}uyJ(X&e*%>wJ`)S;P}BoE&L%c3j|m-b@uRqRgVPIh@5+PWEMOEJA-!!aQaUe~ z2eNwgP3<5^2|BpfhpI7h3vC{>N6)_g;2713F6b|d#g2N|HNg~qzn@M)v7{z1FAWxR zObd7%ms}MCBez^3##oVYbxq+8FF_|JP2;l}vBFeY(Dijxr(m}eFKPzR@k4dRbL9!- z6&ewu*BKiGISG}>OB5ux6RT&|46i0k`0Zz-u-PA{K*0?x75x?HsjOx!U zSn20LT3^iKndYJ1pyjaJkz_h<#kAvhj#8(iPmGjkx)>7JB-WX1k@N3%n|>hT@*WDg z^#v^g<`DwbTPdgaVTGe|a>BID!91-kLc`naQ@lGJ;pd~}zZq#=d*dDBm&IKx`4eV5 zy?tszXb87=9DaE(=J_};=i$L2J9qGyB**O}qm5E1U_(R1BPgYD0?$LN^=8t%#9Sd7 zaktu+p~_ofNm#M#Qy8t_$hkPifZVpr2t5Xdh@HPdWtw&%j6mD#iGKq32j#vdcWlw7 zZTdSbZED?YyBIX9%NW%h4C?ceLQe7# zUZtf?&lr_ufv`1i(Jo?C!`e{Nvb$f@prjJkF3eR6pdWp#KbQ}B{)~nyGv1L24w~U0 zG0QFGQg0Ua?J8VpXRI+u9nD)78r$9QXt*&?<>#k+Xz9-1?KglUuZ_;Hvm_e`!p98b;7h0{*1M_@^f z(yT1@0!3j1Xr95H?&H8Ri+GP$=7~MB!G4KPBYiw83zsl|JoXzM!8~G|TN!hpi*1Nd zt%KkZtFP~E-lmz-0E*!>Nss235`$`cKWtTxR*4!^+BL~7GlJ_XCUQbS&8P~FurQ>y z=Ve!r1JQ6Ge`nD-ENfHtl*LTv9eY;ZgU_9W9E(C*QWugACIBR@6ZqvEF|k42YnwSO zo{}6-5_R(9HLTH#y`U9M`feKJ_{9z>;ZUpcd1e+mM{}T+WsQ$QTydqKZI? z%wNCK>os#((O4ug3z!*1e;+--(D%n~g2Kijn6(g)k$iBJLi~ zl>VA7m{#ODAoPZm#P$6e7xpcNKRF=0Q|8;QMELQ}=|DKcooO}2j#GO5l!+Fl1ZaZ_ zji-Cs$vZAWk!D8xgrWR1t-teY>9vEk(d8Jc*e$cu&@`j1C5*%~>qR=V(odO*JUv?L zEl&pfP_{HBd$_ETb`sxM+Qx{jo?gF6436A*!6R}uKvCp<#OhMcb&UZGH)8b>ZzR)L zuuoLVsj-Z&SbX_eGrWBMq7EVc3PNLJ-Zn;kEyu>@DRa|+`YzyyIsT`|UMm$i^0jTj zOA<}X<3LUIzbHpb!G-PMsqq6pvm~2*6z|_rnXe89q0!s-3&y)@OPFb!9PP6JGMcq0 z%(&hVCo=^$`F*CCWfn}l_QCW&X^zBP^PWCQ-5#}LEm2Re+We!7PUKWDM{Rrft6Ei| zz-J)}Hn)=*UB~Zyo5|tZL9K@>?A_f8zI?q*xf}NAp9H?>rJ~7C;vHDtenfb?9q~ey zXWhd{rKIb{34E?wTd>}hn;l%5Vvg|x>zjsOHFyyP=t-U3MV@Lm4T4WVQgCO#y>kX& zeO+Llsu&mkA+K%&bV3=jbLwyCLAwkwXK)M2s~94z1e&<>grA;G)&O+N`W8zX$It}E zm6>NlUiCTa0m2Sx1O+4Kh=W7KPF12W1|kl`y^d8yGeimN}>YbU6hVik#02=&1MMKs^A=y=%{4Gqs$ku-hk@lmMwoMa7hF4^EKg|_w zmvj-fRN>iLm5Tj3wtmg&WFhT#-xArxOw5SN-)6>(vDo2J4$!ENTTfc+&cM5P=hRE< zzxL@YcksAggZNFnPED^UQ`i+^zuEJ;5}R@ivUvp>zVP6I8Nu}z;@CAZP!MW|?Dtc4 zdh0*#nu3S^enaNp?tL?&|#;$VsOxKaxJcU`ZK zugXSw1xkq_YnU-??3!_KX!F%r?(WGT-(WB#bdXb7VZ#dYv#jZTSV*{H-=ukl9v6rox)LlQMQoXJjPdBgUNw zPVlFr=pr#uBm5DPUlOz$@;(fzKce|n`xzdcoTCkrp8Fe3GlbdN z2MhD}OTD=XUzOL$n)d|T^$P;ahVn2=<(>z0lep`zrLEcR5u>VJ47Q{|Dv$EEEbJlBQj2FC@cb!jIL}p zo(uxp+B;E1NOh>KNhmW*6{&xZ&Pmgyo*qU%RUB z9ImyrbT&tBoWXjyDe!8j=qQVPLL>JWilSi@Lz?K;?1t~vuhuY%5xw=-zoL`dTkFY; z?Uit6qEsfDOqdLpr8T6oP7sBL|w*GiB63VQybdn75dRr*^ z6F)6SB*wo@;BL2f*sUCnJCiNGlY&F&3IEVe%!WPDmnLO_kKRG1OihBb-HocalF1Ul zh;y*sfJoNe(7Nmzzg7dnlsGY0e)%Uh=BHoYnp%kP0iA=~xbD7-Dz4FGQdBXmO8SH0 zQbg@g+D~-uJwxz}yx{>NRk!(=XAQc5b&K+Xw8qL3E0ovD)*;iP?i0PHIT{7~)bexQ zeCNV+ehz#bF-#(uS^AGZucxnYBi zFx*MAK0MspuZ@-}o`fs|&b62cS@YiFMXUi&cMIHE&z#+0Z@1n1xkWGxB~{J|`&RFc zTV1{+^%Zj8jH+m3__45dn{}coUdFPj9H!a2_RCqZ%~d|#CPRnpaIe}XgPON{(R#>Q zQ)EB-=_xbvF*A6o%+nVa4os)a2pn=r!x?q7-ZcvXItY01j0oHQE}=u?L$ zAk;7{InlK%Tb`a=)s&s-fyNWwvoW~Z5(;+qZblvp;MX?xHb8#=E^dY23#}{PXop|z zcnzc<938w@?rKlx3nd?tUD51*%;n;)&Zd+ZBjd+nRy07G@=3l;T^&Sz*tKIOuZaaA{+)4wXCc@`^IRr-b=bX| zQGbN)9)X7(-Fh!AidzjN|5NOu@3LIam$m&VokWLCU_#S2jxurOB2MU8W`%z@or9Ko zEO{&d9P^AIV3M1iYP~7JMhb;I8t>CSH(>+GDI=%wX}vVGB~8`Y9v$i zLVjE9#SWO zK9}e3PzK^!@wMFkXqW9TIJxZ0?C?|zenh#4lor|6>{Ya=JNbUWsEvq3w0 z-y}CuGgQ-`)AkS>u3$G3nV`lPb9Fj`xSd($d5jp%j)<)I|BGqJr*uE_!5a6EH$0h@`u?t*w8uR$UAs4KE{)M#2J4kIHWXC} z+4M9KJ;Ou**kknTB9`Z=V(nE9)ZkOT}mjk_GLZxVH3XASW}1ejvOmqRmN{~ z#G_Szp5`@G`Oh`OYbd==6t;cT!R^xA(n-y+%7g@t$Mgh>lh7Ai!8!OKqwr^@8U#_&6$ z2|A+px_7s&Voz;1)sV{EenAH}-F9)`x79wluPIUz=h`Wrh;wG~q)-l7&?6hv^sL?D zvMuq0ee4L#z3d$lVhuIPCB?~CXK&qj5wgF@Z9qz;sHiKLQ)$F7g#BTE?d!t~;7}FM zfGMv`D$$=yF4s$AunqB?&^I$XuOuVRk&_qS`EEZ7DwA9&v{+luTn~$jZK_G`aVKnM zckw5yRB7Ah(K;(S-B+QR9VLOyO5)*C*>qHksyV#CdS?&JZT%icE0bt(Uc^C&sIg8z zZuds*jnSS@U;g+id%U7zp$1FZO&bjz%Uy?0X{p|>G-XAR*hlG*v&vj7M<>C?-x`o8Cu_um+=U=FrL!HB zZX=kr)$JB*hP$HnI-9+F_nN}1a_U0W9*yFh+KYK`i!E)!O=&6*1ggJ9T;Ig=pPnt1KfK1!R@DaqE&A+3$kq%B33Eo} zo?~09rwL+doqK-a(~HEcM2x;RN%$+TGf-Hc)p4127rQ|BUO#oodpijCj_Z=CjG^uTw+ff#~^@@PN&^a5s38u&_z;m7Bp-xp5ZEh3y#J z^W%_`eLZgJYuLh7-`=MuQg|*g!_6|K%Zl2$tdH%l#hCD$b zoN~cC6vclIE_GGG&+QH}=g(%_vl-rB1+SEhCbT<@9^gxF{Ls=(Uc_u3+Eq;i3xFnd zXTbwYUXKG8RY*R1v30CHwXb+McqiGi=Ia3=Uccm>e+;Jz8TM zsgJOhmOz^f$_QDMj9PiHG;Te$-To4aS{j1axYo-J1e&@hkvt;{`Tw2NYIMUR#)IQj zl1q~GT?#7I$i{SlS@1s?7?Ql{6Wl4M(K!ZhzQoJ6D_ckQ<6TfaC9^A)(@0Z5I*j?Q zCiE3RHWlLQ5GXVkLlfxDB!c2|7p%%E4BoYHlQ7dilWyc9c=Pw|vT!lj!7O8F%tBy2 zd#tNQ-PeTi2z{@^$3HA@>KeXYKK@`7Q4`d81C^Fh zNk_s35Sf#9fT6zjD?Ii5^v5_K$0BBiTHzZb(w{F2hg$lk`O6R6Y0 z(nP8$I8ASkj>+3?a+Krss;0n`F~cme-`R}UTYoRAPw!*^?5NqXpYLjvcNn1?IKqitiG7D z&bekNF-}C|*Zy*YW_#PVNS=gfhQQgwo2@5XYpkWegXHZyUPnXZ5+1Of(@GCCWnKrL zNXDav`!bSz5(3d9Kt15;K|>z8hsS<=@9$zD>VzMi?r+%TiP3$nHz)y5yJGSZ%WgS` z%A79!HKy~cJ^|nyDj$q+LrIOK!x9!*As;*_s3(2vCOB`rvtDYrVfF~|>(Df2S}FB1 z4qdIE+)PlGG<<-!dV$z=^v4)O`U#hvnYOxo@IC6SBKq7uhwkds{Ac zWZiY85V;}sKSErP*mBgz>cehm2;-+I4Spob7x`F;jZ8RX*eKcbg)wY2rgO_ZRU{Ri zUYcZiMw^n2pcR~gR+ICa?PgZcU>XR=tmFq|9#9Q(sZLVepxZa|h*z;-DQ7yEG@Kxv z9eqW71jkHpHEG;uvIQ!~a=mkW_Cx`u7$y@TY+@fGB-q~Ak@O)|hL`E0{d0uZFPz#b zHO=qxM#LyJT*a`-9R0f351&54N^dZJ6^Wq96K)|Tw)f4xhajt3uzSRYretY3l-af3 zs-0PNIGPjpgS#7pt;j8nbL@)15+{F72Q zH3jLY!^Un0lP*g-v^6C0b~$i)1;i0@;#AB|*- zf{+_i+zvTb!Q{lOYl#v@vSnmis~{eJ9wr~tFPW&>iIb{6qUA=1G1Sk z`Z>F2cpQMZ2X8zJ<0&e0m|*oOT^w7>l3IhOGFM7Xbzw&ZTS|Trlfg)lLDn$uP!$kZ z&nEydAWqJ30I3P67_?U6t%8#>q*&c^EgBN0h)X%6kR(cTZ4<=LFXNSKBYdbN%%oBHIN*lBLXUg0fAk;mSfo<=YrRa2NB)AR|MI01X|6V9~H1K-6JXA4YW!{Izy zEIU{Uo9!fBTD@`)C?_Pjo0;zDD0;{Dp4N#8ih{HG2rPe#8h)D_v&&!aqRmb(h`lPr z3jo>Rurk??EUMF7hu(Pz(rM=rEBZl4t@szaiV$V7G6s4c^1A=~!p|4G7+MswIfyIk z3YpgDfdLWu`~2+|OD>rvtjLX1GF)43FXz?bO7X~ODl>AgXTRCdg!@5P zWDEACc>!NtT@uEXYevNmgA^w}VN?T-ZmyN)flo^4j)%g~7R2C}<&`<~&e=Hd5K(tI z5Gn1UgJKnLCMEgshi9z^?U$5WG$NY`Yz|KYBdEj*mYJ6OK#1nZ!cY!^3!A()TT{qQ#HSoQ$Qk7 z7n_=X(*{FBBLA$-aNBUFWi8B?x6KHp=7PJLkpInP{|JWYP*R5XHjA#WtQmH%-GTTS z;?HgcYJCEzyWw4P#|K3YUIe^R<&p ze3D#(z4b(O-pwT|x7>|Rb$giw4-sg{S-Jj_++Vyl1$?&WInz(=PJ4)y-6@7E_GUgluuPw0pXf)U`;Tnt zv!MJUu@(;Mf^IuRBsJDZSO4~s(w53)$`%bDdS5+)RUWP~J0hQX8&<|KmRGhFy5nuF zP&r#svkXInozHuigqcrt7AY7@b*Kc(?&C%}73W|)af|P@&;?q>IVU;gov)^=#f40t zo>@Wb%yc7zS!DtRduu(xKG`5{C~JO*5J?o11-VmIaw@t#!f zS}dG-0>kdSs#!%KN46dfK~V-tc91M$oK7*U@(ipd|Ain|ZCqe7X#?KuEAJv=<>?l~ zFep8H`s)V1+dW5N?yoe8IAqbOw6LRV%xcpVxt;x32c56ggPFiJae^LdL10pwp zd)qOkY^J%1&o12n>qBWGqQ0OlH0FUXnZrLIXMGP;P$4OADih zsSJVnvyMOb1o^dibaA|)o9x+uD>e4-Mk7o3->HPK@E$aCHSURULOPZ!#w{6?D zZO*jq)wXR;+nly-8`HLJ+qQOp@4pY$L1sooX5p$;l^OSww>$iP>g44-=>ycokUiMw zp%kb2`g^FQq^0cWUIqUqumgu=vahZWI{2Hv(xCz!qnC8BlD`Y0VaNU4Bp%!xS(i;S ztZ3J-ISgKtJP^_JU-u>DDg`M%eM|Z$+`Pc`Eq)RqMAh({@xpN26GQoh#Ixv{o%CYQ zO_^{y+kO^Z0chIwX+fn;0Uq;L8pHn>C)tA9->D1pd=j4+p*p=$+dO!SgK8&pR@IYu ziWkO}#SPFO%jvK8_y%tUpsI{ALNA@d_uS@9?zq8X>CbNcl@|7BTeCGCetX#sY3kD?ELH36?~{c;zP%a#^QoI-%KZ zktVUzFKJycM-!7Rz>l1l2LC&l2tI;5{JIt4wDo+x5BFaJFKI2U357-gnEYJPa&ZWFg#+*%E7kUSTeRo4V4+DYI%Rl`mW zYHM*cCOmDlOoWb$z-mWMF>mFVZ)D#<45I2*6*~y92_O=)ySqR~cVJd&h60_9NRl4x za`TYhcPITppw;^ok#8ZZxtRxdhIVRQltwO+ZzeNJ=&(5|E9Cg-pbv7k5fouYtiuT( z=-{5R%WWgZ@EDB>L-J=);Q+_4Sc4X!)(CXgtD2>laMgX56YH>T0U{uAwN8E>k0H_< zwf4jZr2q^_2-8k)n+~5;!5WmN6;dEkL!7Jq*s3FUA1tx2}-WFqHg;@eT*~ z{%wqfRAaBtq4@#73Op(-2q-JFxmLcb2LsX1r#AyAxPr27^8kjbj1llxa7k zVpQlJ^*fdP@^V>LTz&5nF4W7u$7GIQAt>0JFwadY2gc1#5C_2tw7|p8W|OdT1Px!< z?kaMbdeTbs&A;a1J>zlkdJ2|{w}(GSR!Vbt_gDo~?LE%6rl4#vIuxy;3Bu8Rh4Vy| zOMs097YDck1~Q|R0P51RxB-fa~qDO)uoU693JUJ!$ACJ}8hQCmcO*PFY5 z<$iTg2*j^UDn&NEXH~)()kQe2kGMeJd!F$$jAL)loQUYg(9XivcNd`v{kc2yu`y6o z*>G<%%1>h+QZYRG*|TXZ99?h1j0kg$35`hyoqUu3eyESey*B( z2ttC7Z6N*ZJfls5g4yw_2&U^=veGd^9(%ci9-r|sel59}^N@7-XaQM?*}``K8q*wz zJ5^q}^_vo+|D?mim4dwXIT;?Q=<94|g3W9cFt#Ikp%egD_WKsr@;%CTb(o6tvHuh; zMyS19CKu$~K}2|Q<4GYg-`fV`yF}03>GQD4mu!@zS-7p-j|5&S@h_V%;dkl}c)naF z@gF=%HGBap=BLy-mGR>9KS(-F?tgSN0~q3el)9DX;Wg?HQ4Lf2gQ?3$2&SKZar}>{ zcG~%$(}xYz8e;g=>!-2mSgTMf@-5L?U==aaITmOJ*hyb@3&oFq$*V3UUg<9}nW`B3 zOONr4m&BZsei)N`-9LK-kK~wPO{DFzzHCX=a)-yU=b$^|xscX{h*%vU^RZo&b$r7D zI2H=AkaRCc#sb zHzX0OcrdF*$AtHE)M`qo#diG`;uU56O_0P&8@)G@D+eY)6Y!Jcs>`m@r~8&{4sl?t zt8*mQ_2d@FBR6NgKJ6Bq(4k^*`834jvXcX6{>=rCA9QPL6F}?ck-puY2^+V4BnFDo)=c0MZD|N@ z1iJFj>%^biPYV_2PQ~*hkam>r3zyhg&cqwb>#I48qKL9*AS#BN$!c4zp$hVkZ=lrQ!% z#ihZ6_X_CgR;#pt#FL)PX2ulDvmuss0A^ZQ_=qg-OwjJZt&h;$A8zjYWcc@6GjexA zoO1)TqtxJzpgU+6O5jlwnH@&t7d zes5f&WDInG6d+I57hqt^!?1|OZ|@MKen0CcJ4XM^=;ep?kL}pWok@Vl(e{)*ihyq9 z_deWmAEbK}ZT~#4Io5|sfgkCIU1m56<(IzPi`w>!UBZ(3UToN|pdNHKVW+^!-#}1k24wmE9J}5}w<0T464hnne%uIG9}P1v&zM5I#KoU6 zVz2vfnqT`(FWp(Y!}z-A6kiNkUvgY`D84zYiot8$)yyi;Csh=uc5W}jjyI%*kD`h% ztlVKhuYzHHIitNU`UA1zyVD0mJg92$c^rKYt1xZ;R{@eU{p(m#)^1%b`e9QPIleT-GJ{ z>*w9}j^a7-`PGM~+d7X1UHj(tPLid&0J4#fOwrNL-+)gwT}OF}r312$X@*mzD^>g^ zZau@vm(gzUXj^0E`uxnb2w~3ISdc|GWM?}BWEv?(ckbs;uyl1+j)KxVgngf##1o}r z{4iuCWR)FhwEg(Etq5s$YrYM*1W4)~Hb)Ro4kaS<^EiG~jZB=AfQPHZ24MA{MI*w*R^0fx8)RvmWW>t6 zs8^v0=CcQK#n{~vhe=zmA+Xz%j=f%kJB5Pb1Aw_Br|ndr07W$?|QXF^d=F!jVq z3_}-FpH2`^&zmVPt_li~J=S?mwQU$mDm-~tKB7xf482bT^;cA+A!dw(G|Ua~1u4h< zqqtDl%U%KLVdlJgb)rb{??P6=pyzo9%x3dll^3ZS@}M;=XOth}9p4pr#CQR@jJf3t z9*C)d5@vBbPG~VB_gyi==Kc_QUEm{;tg!l#j&cg*BIjS)t?2MI=o?JP#JYMK^C>Al zer<(Cmejz(N>rIY`I{?K1NX!4P`!7ED`2b=G8eT&_t_Tm1~f*xPszj<4^bQM)sDMi z5t~%&+8!rW9@5rn-}gA5Im;*Cof6;2>~DfKv)`b)1%7?EIgUt&0B_)7zcFDng?Esc z@6LXz``yR@G3RpH{4Xschx^+rJ$SMy!uqRu!u+AH?-5kzjH z@cwJaE@*dx39y=9_PUS)`NY&#j>Mh&D!13HdtcOT-tdJWLQ!b%FOOp>exDvq27Y#< zIo}`gAKx#ZlC_K%g}xbC$I~}dtu6}E6&7FfFUq~lJ1*F*R4zs31bi5vGM&N&`D!J; z^yH>4BgY}tHq0yojSioGF+n32Wv$UHdh&ACROF8Ni;Gvo2YDe&Efv>DStKCx&)4CG zAEd=1+}SO!c0I7eye{x4K8Xba6OU2HG;t6@pY2Jh#*>l~EMZ)hvaQ%{y~fwBv$8 zGp@R2QLR^A95o0bJLXe*OC=i>*JDocnv^ZfdX&y_YhC@4+|-renYmvpyWZNu3T#kN z^`9GL@?OMgr5Y3kMh>YeO6ZCb?rd7nOHV!DBU&b)SampBT}H$ zbIc~cOHitEB<@ru+ADCC(3Jbv*Ax`X_E}1PYSM(0j(ixUt8Wj3D}6FxtF%-wHnaox z4}UE9FoBS5P@>=z<$?L5Xo_njrljgwkr(ZNLkO?d+t;QYS_O2`99uq{k?9`+{r4aK z5k!~SUY1QXcG-5NX7|ChCfvqx&~SB^1Y1zwx{;JzUPDPMI1q$>e;Q;RRcg&S z7gW*)1FQ1RBFq+#lpCi++#FV+{`$nmv19Xq_+(!#v=xw($V}@*B^E17_--bS9HL)s zud~!o5aZ}#c=VcrvU}jR74H%p;TcJxrEp?G!45lFA2 z_NI1^(S)mh78BS!g+!I!>={BQT*rXbD2E?Ky+#Q{J@{LcVD>fo=6T7L@cnG?lwbP% zX>a{L&LqzWDOEi$sQ0;DKK1_Wda<1#f_o+K=Zl*psP}iy7KHgQAJU(Xm$YR?2a@qX zL3BzIi~Hs3ixbOK>UYIVs9>c7P;abeEbuf4sdPVdO50Vt#nuo4^0Y>N1BGNOsj>H+ zu>ua{kmha25?e)!pzQAlSQ?o^!BB(0r+9sk^6(6^2qtp4hSK{H*BKGodY@DM_TU15gCzZeT)L&P&=v1$F!X$uD!07Fi5l(aPNi z4Yj}sQ!RAiJTC{duWz|_LvO-bbRu{Yv`AA6wKC)dDJPa-8G~%!i{btT8Ch_~%VQah zX3MF`L+@&NYly`bpT};Y4-`E(LzEi7varS@6!+jbCxu~XMLw(MD((*u@hE^X-nhv& zqI~i7$cZp&o=CRe%-%nbj1kE%$p~2?j|n@bs$hkxPB}H<4FnMeckp%Agz2uZI-)Fp{^&)0W$OW&v&F=_xN#Ak|Cdr$VhNgV6t?3p?s5P;8^ zr{}y}7giP`;(ppIT6GGku^A%1PqIQHk(J&ZCa9@6Hy=y1{=34lz}>z-V7gneuEiP8 zXP+_zZ1kFVp!i~-ZTQYSjN;O4yEBH5a@rRp+*q_aswB6-jKfjnK85bcI$6=8(5`Tg z5Cn1tp%J^RG0f(Ezx(XiHcz@FYVEB9dN%=cZyVl zd8-G}ZS}3RfX!gt)%lcdSj4)RT1||dVWXLM@3JgFlC{p zU#C+@U0TriSzSY+L0_A?M@#If!vbOnq}i+bLCI${LfW>~W4QM`D`;H{_#0-Y%`)n_ zaPLRg(8Y>Y4A1Xx%ZYza9WQcrPu(1*lH{8|<*93SbA46pVjq32!;Fr1$bVOqEJFbE zb#p5x*J^750e77c!7~HRS3|=Y^F*9l4%_Xqx0Zi-7Qr8j^~nk7D?gb-%=c-ka~mc$ zrATqU*As@f=0neN-Vs*WUN?R)#Bggt0|nPKmF$Zzgtmf+`*V=My&(*e?;N0I;w! zZYQA&A|Zc`h52uvi5m?*k`1GZ9}Uog_W4b6xb^DI_YEtzw5vs96FS|vDu0G+$)P!F z>@iWW)s6ePfXPHiRsDm~W#@l`akL(FzqqyA2Jm?;vubKHi0=%chG)A*On4T>E5agx zjiTGzzTRmP`nTe7>YmNjq@^;)bTwnDAzFe zHH8pE?DN_&yC9gwIvTnBiCllHWt!jw9(L=T3=ZkFfi`rOhOZcu;VDNZxA3FtPQYJ{ zVS$jEv3PiOWFHS)U|boQ;8Fa61x!U0|DZK{)IRBx$93-j*I)n((p8VtA_+c^?eMh1 z7#XDM8ILEiL_F%QRVze(LmJptSH%bN6sbBKO`?3xy%FMOg1YrMfixqAU4iGADkDjT zE&H>S27&xmP`Eh;+>2D++VQrw#5G*t>Apv?D%&iaVEQ^%FfguGgYPIt0K~pOc|scd zN7d`Mye!ACcm?PRHj^@oyEWrH^4v!Rt-5vg-{;{ewgS|l&;>Wbce|kFu1rZwhl1FX zOmO!na)t>g>d8aYWH=6gCmDhm6w|7LVH|6Z`!f8fAP`9wTg{Ur`0%AL1pbDo^p58K0a;?h7)p!A-$+elr)q z5_5ui_lc#m@@Y*PfiO{d$FOCGcFf2QM-s9EIv7= zo_e95@KqC97YAVe1|_mjorT}tW#B@4jo>%co@cTv5mIC5@PxnT0cnRe5RO1O8e0Qt zO8&DFf=;Pgq_v>E$qV0KHt$>FHQz2;PM@LCl~?&oQ`#HAfk$~}sS?CaiYi93PXOYG zeZrI`t|)|TO0I%OUse*;JezLCHs+p@V|`#OvzUf%J-c9dxO-P$%1u5xVt701?v}9DxJj$-Rj@gXq{K>mWVkxXIZpC?|7ZVy5 zkMa~C=|>3$)p!$aTqA%SSB;VJvpmyeVt$Y8&Sovca^m-BDXzk%(?C(zcFG-Wh6DaW%JsyEqT-p8 z-J@Jx(*xB)yd<4=Ij}t{^9JYQsjc?DAE)~w-U@afT88$;ur90K8Zv| zB0nV0>%9JT>S(b|q4hyC1IO>o8%smNpN$p>D)=BQ=_{VohN|BdL~8w;3S`9W!BOa^ z(@PrxT$CtA2>(0}7JM#W*|Tq~+b8e$9RP;|9L*QX&BqkcqpW+qhp~!fM1?Ik2F=t*8s3YWF(A zT2BYsr$L^OfSVLdJ#rU8WZ{+!H@0P_Q}UywQPk^K`6B{ZDQ!*&taz&bHhHgXv~=iR zlbBwU!7TS(7_nG9Vr#VQ?48tc>6b51NsyTjPUcv1xv~FyRi}`?-<;i8IzA}ZAr~PU zl89Lb0a^HnX6UiO0ymCchFWbHpVIe1T?J#3JFDJGVt(QbSY5PA5yGr)<}E7OG`TAi zFXR?JCZPMb3upeD`mvnQ7!G2tO!9ykvqS8Pe}-%!(G_8JUond#X+8XPQJ6KMi?^b} zd*u#Rb5*JiJ8IG%PvtB|N!ww{2_(=l1)|+Mil^fop_J0lAS@Xif@rX~4v=4gn9!wn z5z--Y^I_Rp6PDHAfyGR@9(05+OxdPXQa#He7FWdr5!OBp#jj|?R%J&S&47Amjw|b| zBATcBi}K)sDS_5GGk5Aw6S3{0vqcFmVb6>bG=})ta0rb-n&P6^tkJ$E9?;B(K zKknr9h~MM+^8PRSDGUvs^g|iE+o^s`$+Has()hHEyNB0gFRhg@4^$;{2Sa5p(iou| zh{=Fgv_O3bYfC9ip)5ozoEJRQjJp5%2zi)oP9MfE7r_G1uJ`vghWmpWqZ9LgqS|KtGK zB{Y24!|ZNgcF@*?`}IN2tUWUeu#5of0@8ScuH%JENgKDQra(ZiojeEZlb=K9++z-Aq_crNmCIDjN0nmr$)CFLPqONCf&^{8WQ!Rg%NpENJHy@?8?dIvP*Q zb|;MH86XUCw3Of8)5S*n&P<5&Tj{i&GW94mYo?v}ca2$d>31<3UAvYEeL!w{d76GV zgh~DpV@LoJJs&*WVGr@@N%DnR%I;*};cab}e`{a(BnWL~%tMR{-9LdT; zSB-f)Y=}JF?-znm2qoj|JA|y*L3^Pn;DD^kA?0%ly(N`Lj3QhwRyBk68gYRKB3@o? z^Cg_KQWg9g=$$=eNxSu)9A@F12_>?x%c7c3erNF=wpchSu2qHc57R0K@*T~!`y=y4 z!KzR>8zVOnu5#q1H)CE@D4jJD6lQxny=e7ot{f4Q|)U3h>>| zuca`85*@*fT;u|d4q0fol3w69j0q?v1!r3}?0t0isC5{j8I?1UnVRVBmVK~Sm%iP#{lhDt z`Q%QF=Nb=YpN;9#nKG%l_a0OLvyg}ihkaDv=PVrjlHfEU*CAD)F@A`0jcxbaGlaV- zvWH;`nz^kb7*B;yXo=P_4$0yD3S$Lif?Y?dQpPJCD5h?pRIQ}RN18O`SmC`-N9nZA zvZhfyZqMhSQ+xm&+ify8%vphaj|VO8rL<11c4`F_UlDSNEaj<-jI%r|9tS6ZTy*_> z@s4Tf!`TXB%CKEX+M+W;#-ClEzuczLc<0jpBA3bQttSFf>^wy-BwdKkN&?um|J-t- z)6mr2atUdAJGS1kO~f$L!Qr><1_3{7m_iH%i8v`NhySA}AdtE+iVD4y4j9)W3Ra%dE_*1_}G3Ks(vpY#A#|Bbf3_tgxOw2=15_0Z~KqJ35Q!bIXZ5>lR(`SEvJJ~>Gl;p`p zZ0#Q?uYC(_kQy%M8SOZu0K#IAaHKPA*oCi$&E@Cw_Up&V3T@~1BJSr<`BPf5Fp7P1 zGjlIyZ5heyI|M^Ea<80!UA${;-Nc>sINNKbwgAm2MUu-?s^TLKgo=QG!~P&4)_3vk z*5-YY0=w&u%yXz%>~qxBwG@u8EA5GTjYe$E{UvJoI z&owgl%U9d_eh}gj3jqgb;09sd=kzM8t0%qPNTEY_8*Orq6XVJCf^Aw;Q{$@Ttt()1 zy34cpiO)gtfWG`aNp=dolFf%pe@BN?0!{EtKw!p}e>OqR#YcZgi_-lcLWWmlWl^%L zuI*=qmO-7HezE19J5W&KSIB~HG{R6?7v9N1z8(jAjMYC`fa8Fsh{?f1oA(FNz~>07 zZU4p0J-&*jH+Zw|8xNEWJR7WoY8wK{E^2+bGr@1$#%=2^ zo+-nu@*?aCwm?FSulYpFa@z)S=25!B1rPM9@QYl71MIQOzS1`SJq#5j+7eF&l-n#_ z1}Ve?Qd)N_8KN@4Ai1>GrS;<%e&44~!swksaG}x~sj^Fwj-6q@HMVC-;4xs_%XdCW z)wKeYX=D_6kmo2e3CdQ{^T1TrQUO`UP(C1_vgs~_MG`Vh_tUlSE?>}V+>6~a^g*t` zjAm`yh24zyJIZqS)4l#^uldzBlEBmpmiD|1d>qK5TuwY5jLph4ob)VQm?~C?DZ{H! zyJf{PGJm}krZE*7>y+k4xUXCxWgh8|s01@ylR!Kbw5B7x*TNGQ%!1kjlwGGSM+%3- zqUV};hB9dM6!%k!<1vod0e=w3u~E|6MohL#I1lKr7JGCcVzGoe9%Igmn-Ovnm*fCp z-R0j5ZdPoHSqb_^?^gq6qrH`2M0?@nG@9Tn(P*KbybF^nkxq^Ac<6S!f11oo$Mr|q z3`afe&zVHV!d6K>1{rIpg6uC)PS$U4bQ znOgq6=h|K#8wL*P+FzQ{fZ>7-cWNc40Ch|zw(_v}E*3fIQiS&IPg`~%X)9Wcv zQIxaSMgLveNZ>&}9~ba|;}LNjK~tdUcme@hV41!>0a+?(9WJK3Hf1d4?+$!w7np;t zj0Jr{E8?<}900d30xlJW(vPq7FHLgovqNxt2pv&ne|#J1DyXSpy@!MB_HZs+iC!^A z<9~RH@OfGor`o3l`Y1C=2w+vGc-#4<=+uv0eMQxXU$cbNpn%N`-?B8s9^s|6)GvV;Z55-LzBN zg_hu6umW0ehs@*;J=5V1uXz}_q*qhgxJ|q41=Xx%z9^dG`Z&9;U}ii# zIYFcGxB_^jnqJ}%Fnp;4F2td?1BxIo*3KtHH^56G9{6ofBX&@y?@~_LDJMB#%izhC zlzQ|g#F_-Rk=!D>W&q^ z6#ZR8cBPRXGz%A=ndvpSLXifL!GwnqLB<@(g9D=IOiq&DuWiL`Tt0@w2s7TXAih4Zbz=s`;u^J6+HB;t5v?x7+~; zz>**NacrI*S&yX4fUt|TXFLmi`;*-GFSf%XhfLS(38YYC?;}Im;ZbLozPF0SNNZ_U zt6=T`IZ8D(&65%2I6;Kkt~@E5!}z75O(cmarm|OM{<*;pvXcu&M3Md-1lY8Gzom1+ zEO&NpRrNB|Z%aro$9`K*^KoEm@m79hFcgx+)r8S208$8SuJ)S9*p(U=$I0u8L>Cc# zqDt8Nd%Big?(Hr}1%bCa{{+zIgD25kZZAl(@hD}IB?&`FIPRPc;y`*Hsibu5G1}FD z8brF#zxh5O-9=G~&ecaKw&HPVye$}?xg$+bDhjlT7tM@KGZP8DQOxuoZo1~J&XiN1 z=MMv^TM{OuDFb82qDgAH&uPRz0ADkuy~eFe&r!-R>XOGTsgZUwwYJ<`a;|lgWZ?K_ zo|s5n=*6C}vlF;X-&_y?z{Pk4F#cxb3o7JuDHHD9R^>3xqn6OwFr%z=ezFwu_ng8C zA*UqpFG;!!-$sr^ZcWjPU}Nm6u}*Vw)#}|qV#p6Zoj|UATQno zy%r)J;phzNM%O8`#am(3upD`OFLLQlI);0Z%KpA9H5u%;*wu7(rI1*cz{cQ43X*5H ze>zYTwgGt@2yw?R*E!P&GDnqW2RGDn+877*!#eKRXTXF>$La$!#md+?ZYakQO9PO9 zDYBFeqkl2HFeW-1H;_+S4qPs*8XVM=KCd)T7}fz1M&uc(YD}hstV*4>rn@XC-9=dn^80`P9X`b5DOJWFgf1A8dcY}ZF zB0iZKQk~GkEa?K%%9W15i{EuZ(Rw4-V)|b;Lp3I~AV4O{r59sI!lKL!qX`T6@r#c` zuE8-0Kv<1KlqS$s5~8NSd!4-c5Hu0c&s26XHt5H++ z8Lg4TLd9F&aE;lSEcge=o8f{N9^ir1$1yPNi1U-MJP6Lkac_g4Kqn|monLFIed#<% z|024^1=WR?UqZ^0hLg>0=)_Xpak4BCIIPeqN0(Y9FYGHECP8}|zPkwh3Xv9@NV)<*?Tnd^Ox--DSjLNa{n@tMngbm>ozmr6K7?nWGxQ1Xfr8;oFtDz#DGxn zguJyV3}k9KSgxIprMGCM3xtx#)4}$Il;D{pN1(sxRV6P9wqm2m;P*!fiL8vQ9=Vn! zIusA#%gkc8Xz^_olIb|YjM~scFQ8oVq*0l&A4i66%g@5Pq8dF~ORqzoe|Cw5lKM6; z238C(8=!e4cLxkXhuLR(VA*N<-M<)+h%6VVhrx*NOq?0>COyeJ zwr5cZX3J$h%+)FCa^>Ei zdM{&lr{vC_k}v6ZI{p$QJi1tfhJbGILE25fn^rU3a?K^}Y`k zlLvc4aS<@_|7HO33FkRB-?%27Nhw8T*mOfVkzU{q)qGs1G@GV(zVj;&V52)m`7N}% zYzsBt&6fZrr9nvSU}?C#E1^dX=Zpq-V=*8rUv#EG47#XWSY#F7mQy{|*1DUwT0NxK z3jSr^{q}lDGqtJLnG)PHLBRzi1L_(~LTIO}nau&8iI7?GHJ4=_4Z30>dJLR0M4RuZ z2Jmog{kp?qC}vJaS1$O(;QPJH4wA!O%VW1bJ|#%-Yz2e5u*o&C%=2O@${4WqPe>UP zo1~l}9^XL{eFUUzDqyiC;3_)?;_N!P`P76VEv24zgbppGV)QTp?B1XdK6AKhg2}(BbS7dga~0O{=QEUu&z}Ll~}QJ-V*vs@>g> zxD{NFkFQ~A@Uc4It^V(f14hZ=tvk`mz7fOAiY|kh%d-YFhWs)2c#iwM@hZhKgEuLc zu?far^*?P@GY@s_L-FG2{ent)$5UbYdV~zcpPBrGwg<5?NM(S!Cm30 z;@r{Y_T{Or>RxPlr#<5zv<&t@Q3)O+w9@6OMCA!5$RTbF;&a2QXnlp!; zPGAb=>1%ahujpsn5)ehap&+^T;Z?X?Ws?O%=Q7W+9R*4?LH9u&H0tBaU^d%Ao^h`# zl|@=*1yXzrMHjFI^YFJr{T7Y!=x?8O9vz$c5u9t)F*gVVe8Ubu z_j4_hj}Y#v`GBGsuf>X31cReFFZX+Y%NfvKkO=WE@_J{b94V4;p*7|QxU)4k-uZP~ z6=kJM^3^ulTM_=NV8p-i=;M6}Rjq~C8V0TB6&i|vMi_HbYILTg@(#D-m-H-c0~IvY zk&l})^_IqmO{Uu8%(x$vp0p_zMz5gXfvtslX1{ zB~}p~U0RD_6tfrNCQ5?l(mG~CM)UXLZ97aH#A53474A?I)^;YTLclKhkJ@`QxE|yr z%InXUWNKH#B9yM0LQx>KZDA>$q}w=P$M)Tss~gejlvuoO4w+8~;^t|5)_QR8k`L#V zM+OW)GtaeSQMWdtnF5kHYrauonM{ zqiL+dU=)DtJphVm?x6v6cE0q)u^}*#d!2Iq0Lt$}g8e%%GXG*Xy7P^`4 zywr(sSlu9q?V{9)U6&l;96|*dA|k)yjL!U24Qp&*hxx4&px&kknU8|)*kP!x@yZWV zDqFLSAx~M(g9`l2urvLd@V!(tGm|dcU&o=$^nXduvytCq3ad-8uzi%2!8@0QOVu_L zo+)@%xsx26;>W|2CD@KF!SS)wlzVJ1ndOecYQg&|<)le7^m(EPmP`xbva^wW77Bm{3g}nlESQ$zweXVrSI=IaPYg+Sth8KAhE(&k5tmOH=Ne4 zx_od|e#Nh}m;M>`nFYJH7aI!P+d_$wBQ*M9RB5E((^Je#H2LSjxe>ss{mF?7>%#@q zr_azO6nNxQ8uyK!?bZ7dX$wdnSzFPkKZosQLSw`T6XLK#cOcA*#M%;uD?$i$>@AR* zl1?w)LWQIl)st!9ugd&o&Wn}W_F z0mq&C=xo{c_yFsG3)pJKj-2~kA&$O|t;`lAB4}C|`*3xL8AfL>jI}$59`1&M-oVx9WlN6@k_uvEw=r z-R0g7`dWG4&q{iIwL$nfZvzK1vY^9EjTeBn@4G=~n8!TwAU!r#7E2AXe!P9>$K5=5 zI%+&W?lqABE~AVO!w5Z(p0>I;L*Ma!SrH@6Gg^&%ajtT7OReUXg_%(urKl4gRK-Yn=Y;=?BtbM`H*HAJ8_fA2)w50d$ zei8P&bNp7%1wp*56&{s>;;l{)FR|};1wpW7SNFGHK)WLJcck6K));~y)M&pF=DX!Ddl>=I{CQB7yi;YqD{jg;A`4B6$8=>*YDmdBPJVEG znBju%Y0E0L7{zVZHDoyIAcqYU`4YDXTmHk1{WzNl@o_;L>Y-sfw$E$G(06AZi$%|f z?~VNB+F;T_FAO$}ziAxx;hov0cRXTM8xQ$s#s#JWOM_k~3YC+M*khqA5ObA&s zh>AwhA_MS({(S%)aBc-Os>}WSt7H}n={&0oRFu+sCN`6WR1{=8 z>V=J?g{-J|CB1No&?>vxW)ras8^H|UU}af-BwoEHC&fi@XZ)$ZA1bP)(p${~MrlTR z?{F7d!4Seu&(`6kgT9(}^*Qngy*XEyw~18ok=#(#d1tqBY6;`vgj5RoU>YiU)GEvb z-V1huRB3T;xJ({BkSFSUjhvw~7oW2OS)3cRxm4$75>M^CrpH>1L2a%d8>xm~QU2V);+UURXe+1!!WRTOuxiuU0il%G|aB&|ze3ewR-H0@RgK=E~fPhiFa z`66x!5s0Jc7WxKbmA7drGDse6J)S4Z(OY#;4nM#F$P+tgvbXI^5S|dm>;$+yPp6f; z(gfaG1ps=$EZ}qfs;yg}eq~ueVza_r<{gWMQO#04t5S+;ZM*`CJQunITx3oNMEge+ zeOltDlul(1%B)DY>xxT@*ueZCqsgEl5-;EXW)sM#k5pT0Aa)*aRL@5&p6a^f_h7<* zudrKM{p~f@hSr8(t>l9<|G2&(MNZsDhA^u-7sd|yIdQNh#=k1~WI<>-3HRzJYrdz^yPHJ|v=2)qhKgEMY24p@;%72mlmn0CTX(x4A;^xw$SDBk^X!TMizsrhxa+)F$p?U?2j%APWc__gH zMF5E7t$QQv2yQkpiV1AXJh*7(hWNZciWQVjAb(7Hv5D>Hl-(*PspgF`&m8UKN7=?w ze5zb7a~HcBvxseHH%c%$%&N``lvmt=raSDdKT3LXZOxO$p#shMSQ zL|#y#TKwDUE0v+>z#2TQm6dZF@}vTdu^tr`0pNg((O$zg@{{s_?w9^vGB;m5QFruC z`dQo*lBn9EV<)SjWD#t8+plA@?j2l3Q1=P$;l}>NSq`}S#NU5~Sf2gFjA3VE@mn7K zy3n}qV@F9(TeptM_yrvH8$iqjX6R^0EQ(2vAzlYTHv)Jv*RiUsG9yP9I`@$;YFt6W zelXuW26LT0C2CFRZ8}`Y)!kfD>_|T_19W)L1q@#u; zsE=U5RDT7K!*l=4>Q)am+L4YE-NtX6UuTpQ+<-t_(Cp2)j~%>9TL9aS zFSCHSB1=ZlCW4p7wDCu{LBhlh&hm}iD4XeM;m{`!I3D`Zt?F8p8!PVqvJU@QDNs_5 zu@6OVr*#YGETVeMq2CrO72PyVk-p5nWn{S-3#5ia&eBh%Zj^6})u>w=7T3osg@2M; zRoM!+Z7#u|y_gEEKxBcsIe}Sp*b~pl?VSfLnGd*=l9`8$%(#d5>^? z-_QkL<#ngjbAmPz;_yXtJzw{a=g(yKYpT>Bfyer39Z#-`R~Khg??{rQ@e2m^HQbdp z`|iOb3%j&0k+lbPlBXz(f(+GdP+oPA(mH#7ex2B8>m*N-d}30=_1vzmSqDO;+q+tS4mRFg^@*|~w?>VsO(fv_W_LTr;{*?zR4n{#uXGgOGqr&>#7_Ubi(h_`@l zDz{eX1p3j~34U$~gQY=oXIJJVr++wtGB?*meCg?(YXWUe$Bb~(G*o6!VfE0sW~`{1 zl>y_Wp{V0W=l=^^Ii$vV^t}r>2D1Yjc9MZ6rCy>DtWJikuXit@dUmH$>wP(zBm|}S z+Aq58-3|1jqq21^Sk<`}l;TiL{S>RQMJRCNb*_R|(+yoy4Z=IA2mk`idl02A() zgu2|dMHJO->P=oQtMvj^tx>wKy!J4X)fg@8(+xkNgdBkJIqB1RhpGPLN&e&zps&c~hLPH)yDO>25E^OnkZ3Qw29}TcUz0a%D-K59kUIHo8hw z`Rp;Qt^?Ro5!r-#8!flWjiN56`!#42VwGKpBRN-Sg;#+FG6s}{dLGM%Dk>>sfr}^3 zU{%_nk~^axgH^{&Cf-TC9mn~-=|=g|L0x9j_>|gaOC`8d1ZtPevh7D1S?Y{I!a#sH zqHshB`_b+*anx`7#@5q~zdd-yd=r|j9@T>sw=3eYU9DW}Iz4=QnOiW;^=?)7yq+~w z5Vg>L*&97r^9!yH<4t-%crf5Hop#-^+caCxBtxvlZR@-!j5OP2{7#7T6%UiPJ)riq z!3dxRFBPtexN5$KEy!bM-#L?&5z)UB_imlu7y8b_R}cNZ%z0kITprfJ23~=U4lCI-xCkQm>C|(lU>;aYr7Hdz=*`N$f5br=olY7FmiYNb}L^ zK~xt*dn(VlejGT$fIal_(IiwWe$PTE9 zR|=IJv^{YF%B?py6kgw*FX)+vH}M9_0QSvLS)}-OaK{3%TO;cU7 z28vu7w~zbCTZg52j4wE)p~JCew}9^hSSt5GoTyMhYnsw)Jbs;yC$y8Yo74j>?xbJ6 z62Ov0zPsIMCUU{u!4Z~SAS`jys?nHK9MJuEL>eoZcBwwxD zimjKSA#WtNQ2g+)-|>+L6P6s`*)EQ%i}&$2JQFJ&%CuN4`Ylk~x*JJ|VbUV%x`XbtQ5{_e;fDb3mO(-3s+|)Z3zNg9u!QOeBC| z40XeODhZXkgGm>=Zd00AgiLkRSr2ueV>nR3cc)8RPxS6g9(csNUt*pjxsqL==Pl|B zYm^qR_qn1|&H_2VWVqNL1lEj*6+nCqd+c01vLZQVRg$PYZS9YV?>W8~NL#${jRUp4 z?%*x(PFss;v?smjsyk7OM?ypqp&SuHPZT;VWjI8Q)KeXvBDl_I?qsj1dvFLE6T(7q zcR(TxNmk})#|HDF}>aAZZVs0z*t*EqW*xofbycJ-RUpe~~l?bIy& zpjgGZy5ct-xynVDGmDPeHfl{o>WO<#oGU#GfU-zArP}E1= z8*b@N7W2v2J^VI~vn;p=cdOL_kIC5x8r-CEjH4CDXg3L7s8JzqKCr9o1&__z_e6_X zn$q%dp)&hq+(gVe2!r6zPQeIq_VhI z&T(WY*RpW(Hnn$1vss+>kAf9Y!J$Ut$-8ebO_-97yrX>BA41h-J z!10TA9Aq9?Hc-Ew!G8jw|wEWLY0c4V`9peykhNk>Fz zBFo*#A)6)0BW1M#x@pOiU6?DKZx$qma1)qb*8uE|E_G^4>+BL;>5NXM1dD9L z*fWGWygqCkRxR9UH9obwb=oyos}O<8y_79W6_(30#M^vB5QmEKN~Q|EtTcS zR;1|&y65G^)m={+$rv)Bl?&u>Wti<@Nign$1SH7f0FKp783R#x9qV#P?!gfT+4iRe z)Z;S41;P^3Qz>F>+obSh#SWt9pSSHLuLz+9aI?HPZfu4u)K~XW+E6sgu5KvYxF}=B z=+Xt@MpZ}GC!|HN+REj%&F{E*j(PEdg+n(^#L>Cq3jmy{0#_*jCe{Kq*GzP6{4P?& z(Jl$ySE)-P7lTtfXTRNHAv@j(_mGXkUaNCr5q2WiJ}4C+RJwjDwMhW+w$wr_jK&f= zp><_MH-4xD+svP`jS{q$O<;G)t%hJd)DUSa%V&_3VAnc-CUwnxnGaH^V)<8993!sEt5D zSSUrR^Rj&&VQyPqPZad|72Jm_5Ct@XV&cpDmJjV-xfwV`?~M9$y~yYH5DjymF9f64 zZU&6!7cP)&p5M8AKHg8S@08Cc35C>m&hw>7LTL1z^L%NNPsujFYo0GnaxMV(ee-;2 zk}q6oy*AI6Ci$*<-S^D%rAb0V>vzsmdG$(5c_Z34qWw)0?G@e{I?TFc_7Ox1OO~C> zNiW606#zQ(&@OWy&X&~h%O1Z!nl*Mtu%nqAY{LvHIjA03Lm$26(EBJ=)2*gp zwD~YM)p^c5p!x&D#FII;H@`{5Lb_1mCD@@x01=WiK47kn3*LiML^!M#5e^d>YDi%) zEAoif+Tyk&s(wqAv4kC(l;iqsQvoP+-b_Xv<_Q(6@qp&25(T5C%;Pfj(SFc?285_YM2%j z*K%9@YL51RD~En1IwF0%j1^x6QfeQ)hj5y-w7gVjQ=C1W&e0sCZ}wRa^bmcYM4g0o zF|yNC9JP7@6B{bRoWpvt!iecwJnqHivCekWoR3-&-0L#GTbXqPm=<1XYD%1Ol|s3m z%34u^UNo)Vw?>XQTfXJ`T|!L5al1S~8JGKf3^AT-TI&txeoEub0zOL$tZ5fmsMR5Q{y?0Y9v@_a1q7Fy- zatxq4a7$3P1-lEjoKo2n`5;@Xp?Zzf0?=viZ7OPU7>tWr21#6PvPWJ*7e053x)9Ui zB&B#gH6q)7bl77y)-Z8~xNonGm&r-iRn>VqDuRDI)JtwxF0*BoR<~-0`qr}&tM8C< zq=W_9sWf*}7=I5m@K&$z_L|c!f_jwFLR^ zvP+K)U>WaJi8-@A%fEMlpIPNAF7QwJ@m-)2D`=&4?_P%Tyvai80~4ssxwvOt0(;p7 z{@;w#e&cTu<@689wSR9?=3gUWe?+#u7i_b+?{(RRZU9(Yt9m37$Q6;5+39;j2qc$jh^*9z0>IB~I%7A8DhtRK`8Wq7%ObtquMQw};(osZDi z7&v&zH&G-IO>jH7aon<1U5{&hrdY!YP;RY;?@dI>9QJBS6M-J0aJ{Y)B?=v8*&tAkMpnpuxhDekhIJ|1?y_#qL(cPpzOOF%W)UBxb}w2P z*Q#6>Q9;zX=rYh@8W0O6wHSNqj;yVRJ3KbZ*?|(a?F!#!X=j(@Ua(q_#!Yq)XZ#pG z_B(McENX;6sY3~CpD{{qum}Ad#<7%h(Zn%#MCh816hMvDajIZ&k@Y|egaDyyK&lFCcsD;`p>Nh* zv$h0%Lp^eQ(3vpGpqtj9vWF;(kOy=t7pX?QU7M(#Jf?Pt4s9oLwYwC|)+JQ4R=V8k zk}kEVe0R?0ec#iun4zgcjab9wJ#~x3$O0pZh-(LS|A3b^6Ha(@P)U2u`9KurfuU~J zm?}df^O7(ta=YPeaZ65}Lu^<-LQWmPKM~A=j5L5A>dg(0N&&%L3PD{T?~f6W00B08 zGK&rvIZwvqD7lV&pKDXps;$+_<3g)8i<2~N*xQ{^H?iMcdDBCXNO3Z8fOCiTq-CWr zJ11*~EQtJWg+n#ROCsAE7ycOfJ}DKqHuss0WZ_dS3{#xlcNh;Xr6i*exQ-aj8R~rM`DE%BlL2W?-0{U*pg#U=<-2j_KiSOdpjLfI@tBa z_R(H^YtKnV>Q1-2ou}&7lyx^+F=*UK?3`s~rN>h=Jdj6FhaIAVgHI)i_rR``J%qc# zIZ<16jgl#2EXZUXBUH?{)#l7ARRovrw7(nJ6B=fx9uYG@6mvHWjbh*lmbk*^xS}PFZq{3zxrAlp3O;t|QB)93%j^`2M6u0Ck$Z z4kIripQp{CMa3H-4H&ksolBJ8c|Fgp5d56 z)sBJY}zvyY1=$k$m(7}Hf}strFT2M;|#6E}B1 zjuI8Ko2b1;tH-EN7QUx3-)ck?-F3_~T|@G|c35KPSBvx1RF_q@UTaY?hnxU5ctTU1 zg?__vp;IoaI&yI|!%ZOIlRCPMR7kQ@bX1Ms*Mx;Tn}!~>tp}nlPkyZw|VDtL~ z^=d>VM+>H;SxsnM0?se?nPO`?d-5`kiZt?efxNDARu&V4e$72L>kX4^1n@&X?I<_9 zuLXTw>lueU3x6K-flHTKm36;1 zP;qg&l0wG&S&R~W0ey?<6yXmeR0kGl(g1x_>u`szqK=5zYZSo%AeEPJbPaTX97dr_ z!XjFgV~(B&L@b$O*oRvvWz6*6`6=d&eXvZfTTk1B)4|DG77tq3Ga3d5Kezxkj0H{u*dW}NVB7#<&V$_lwl#$mWg*48jsM#=Mh!(u&= zcLE;S@2V2#DyuZ4$ym%)HC%4p`)yVGQ*$w#(pomk`ZAn@`?RKG zgCo8jTHhn@O_!Q}3O41G7aM<(I%nw~hM@;A0UFmm6i7a$v0>D5vKC#sj&_wK^bId66mpCN#E&qOY1s*-qPuQZMI?fSw*hLe21?m-R$9A_*^6tkgF_ z;XByna@FX~WmLDfE?ZALA;#3hS-_GV!U3iw2j?p|KLz)R`?-qC;|S}!G~BJHw5XF& zn39JvBckZVx%{l8r$7wVL~UQ}*1p%|E`yiI{w2M196{_Fb+OL*BTR+r5!SXa94v!Q z!d943k3e6(g!{|kF?H^E zV+)k4@9@NV!YFNY$sb$HUkNe2Ns6^7E?`N~WgIcC6ZhOrG$K9%czS2c6`LPlGq3JnR1)@l)JbH_4j7GSq0WA zrPkz1?hn0!#whiOS`{!PR8VaHNMXn(Qji#3WlqH@FqH>E<@YrSX?`-7Thls)2L9-- z>iQyJTiz!`zyKA5xGy7uMhWu*3&5o?qZE>Kql;&P;59 z?H`s*i6K+n7W{cG@Jjm>8p+J4u~9^HU4Xhx7iG(GI3S8Lpxr65x?_qjm7|YZ>H zg1BdM|Lj1b4X%UYY1y5=n`u_+yfcO{-JfegIj88u4HKya3^QfWBw$aQvA#|$zE`Y< zf$sM{PTI1_ancqo;%OQwgw85Qeq7?qWy5+pUk-pP!z_YyFGpF)HR`a}SNc#RP={R| z?wyJ!jA=7*s~Kwp5Tvb_!QDRXZb?aWGUa&oNc2O6!vg9QoKGl5BFV^4wQ;3j2@NV4 zNv$sYnerLpzt%B7uc0 z5jr+q*d@7d9JxPwLd|vO{w5JXiZI@Cw{~k2F5O7pjeN0L=qDWU97cr!TzQ9j0npJo z73HJ>DAL8t!KQHEzbH`hs-5a%1;|0%nolS0Lz)Kw5pwRuf+yg808xC%)&R3gKG~_D zgneHKGAFsj-TE7*MPn$U0WU^v(4gMn&T&e@;%?VK0tAUJ6UbJIw*u*@&Ka58Sy z@~p#s_l+@a0fZ9EggJ=2V2Ryvj3Py85w+Ozzz}-S#qyE4cGoms%2daMOacI8Rb1yp zqe&hdrjm|X`GzlQyP&gkdDIRq<=-A>UY2dmN!QB3mjziMs2ge=gag_(Cdxn|CFjwZ zirV#Qtws~hHr_sEcK#(Kw%rJ5)7Xf7oMjnrS*Npg*z>%A@i#M#1$i#9da5XKI}C^L zaPY=XWwlVz#G<@w5_ZEd#9r)aiwT$eO)b*xiB}aG01!aw=VEqA1>B9e-zJ*zXv;g$ z#cpRh=D}$86es6PCPOxj54ZEdsAjOm!2E)JTlp4KbPygihH*=BA3xuOyMFhu_nR&> zResZ?Z51ejxHa;Xa$9aQuyACzwmQb%oX`1OVs&-eR!FSyH}3=GWJQ{mnJ2r#Mh5J< z5!Y+|1YWSs!M8ObmtIs`Ekx8s9W;_l&{2>j#u<|%!{R091LZCE_I4X?s13HtFEM=6 zwyi1fHbIc!zIQt;GlNyvw{~Wqq{LsSq2rIu>UzZYI4|UQg`M~lgC02@x3Rp{PF>YZ z#8p<{zF!S@O(>OhUMZ}XV!f$tu>He1c2ZkudUu+vq@(aOlf%1o;Fq2J!zb=FS0)_Z z{=d)QGl=ix>kvk9U=gXG1{(Q}S$&4hpzAEeUlim;zZ?tY05)DU_7UG7t$Z&v`r@N=9p+Ke!j zH9=N9;`B*}3`h1HTxOeqSV`_2~ws=H7Mau7XAH zUdd~UAFaU8o!FG7ACT%r}@Q)z?<_~R)#&n8e%v3Dq-4?VRU_IDtl zHqTlPOBni4gP%|R4olb?a@vpmVjt%*h+rQ0x|&DK@?Z7~L}&11J>VG}(O@=}cddEY z_p{cY&G-3aXP49;oA2|&|Qa#?J>)jJ^4xjoUbUhPrUA<8^%W;jOU8&^sM|1%Rg6a zp~3BM&_ZV=^yjrui06M+eKzKNR$r|YkD~V1FwUC}`-8p0@EQ?97g%3rPQfyWGkW1y zKzl{VR}-|n&%eSijdGAsV$K)|tMI}UERs%OI#JaTVd_AwL06MF5Ph07q{a9qP#hh% z9RO8d3-<94B&}4SiJKxAqugKEv))B7R;ASe4oT=Tmk)!}45x`N!C3#x9$OIfoqCpe9R3WuxF6LAk&B?oJ!?E~e?A_G35 zVDPIP9}F!QkJN##1~=5XLqQlho7Z$2i(Kq(R__w-!b_zOId^HxX@f(n9lJ50{mbZF z#8qq_?rpltV#+m*(V?Vp9T8C#5gAa2pvQ|ohKNLTbRW@2YTX4J?9SmS(4E~TVgjE) zO4`e%>Y)y+9hOcj|2WE*voDIx94q7uMGM{(kc{IDXWXY`x{P1|Bh`7iw!}-Lxltk2 z;g`o@SUQ(+A~0v2r=%Oe=Goa#o5JIDQY-Ik)s~nmo!jzu=ep$DvUnyh<7Ihx7|zN( z4*M2pvf_5iC0n3jROafklKTa&rv>1(v4&tif-|fUNT@F&YYC?UM0N(e-vH%QIjCnc z${mjMr`{(avl`cUEv=2Dx&|5Bbh5w`NO+r6NTrJ7mIBv0vPnJgBt z_w1B&LXisB=C;pMbt4b1%&*t`!y!zB2Du!m2c58T({p!+%IpXgjJ@gFVmp%UerX_F0NYpoV}0yrz(CcV?F`z%HIM5A@=rk#2p$4Cw7% z5!t;8q`PWj`88Fyq09$HuEgNP=snB3HEW_Bhbl)(c%ZZK0SscvN)M`Q*CeYikH@rb znN&GR*F5drcI{Vc)!x}0#{P!7^o2x6N4a(mm|P23LmLP z-xsNzHPiw<(>S5B!UQMau z?#Jt-&c9tzq~9_q{fHo6f{&2uz7s)ygpa9@#=+I?aI1*8_sIjcJw+myPzThVy}WE%rA}CH`Tj{&M{#rWOnDJ5 zX?wHI+)fd8R$#HzE&zzG01`oPaGfi4N9qX=dt|e_I z*Z$1N&XLo$i=J1A6vF&OO8_Ea9m{B`1Es7_L{{z2SQ6|o z(rBP6v?H(r2SfJn>|~FWsU4S0wAIqM~aT*=&`b+k1`OsnNEd-H5KJ0+-ef8nABpyLti6v!w^Is zTLiyYpZYw$Pz|+31eDq7jOPp{vXbj)*&O;5t-Qg8$x9HKkn=T7&wkYY0?4=UG#OHz zY?f{{$f|E&R#71Hq5x3l5V6o5k+}89lsbUp&?m==ZnZOeG9zPvJOuWj6PE{TbSGq^ zr{Tn&Xq-I+F4|qGySf#cK+s|GwpDfDwa2uIx4eKsbs)M}|2WIZJ1>f1`P_>FxlZ9+ ztr!M6y=rLM(S0o!9$E)vb-fdpeMJY6p@|n8ALX{)Veo1r69HR^Z&SNpE|;RSjFvs6 zp-YBZBZ|Xr!NC|vFO)g8xQ0}ocE#S^ID3=LGYJ|SFIjb8WZg-yhKg6};ztZXP((^U zZ+4oO?&r-;Pk9f1+VSIKjF^CO;+x;aKh5s~+N_QggYtZWMHB}Z+NiSYF6>=y-$twA z-ioAjmv*gT8t%zGQ0u*Xa0NPBPy3FSpn?6v{TUaFCR94}tikC1vB6OA3DwfavrQD9 z3uH6Jb^iX5nzAhKBQL)|qV!`VbU>jL1*frnmT6%Di#h`1oDgf@4^KnK9nT=3?n z?^qr-=GV-VMfmAf{ApNE4*0efBtN!Fa}9Fz75-y~=Z>;XumKI)VePeaMwzQCJzOlMrTiTh|eVZ-3wzRV@^KG{9+R{QJ z^LMa?*OnH}{X5vgYfB4*eXLjUONJ)UqZ{QEJ6b8|u+hy@qg8K zzeq2?W_1|Ro9)@ zfDwG;3Lnn%GxDxJq3@61P}nEO82!i#o_bKKCVf<;SAcP+<-Chu>*!Jikmz6NNZ`9i zplph>jbouB6z*E7c;J&>nF7Xol9qdoIoA`=V@ZkI9YH>Ugo11HE z7{9hxym#lBkTbW3IVwRivG9T2qTS?f9OsetNI`D`cH+Z5qiPCET(TC-IsXc{O0#3_?=X~ zbZH-|-V`?LG6=^Hga63WvFAMfeY1O4@j-TPyB8YJuQUK;2nP7sBE2Fz-Zc}vOAbLm z8O*{ky*z98VIuDyg-^8~zyC<&|Be0m7wmuj{L6o{yuPtNbhp}%-Gm9Xf;4&dJ}UNUo@PpIs} zCgQ}iO?-)8C_oN&d|2Y$-dY{^Uu@}9)4EDuZTR05MgI!UGr{5i^Cd-~pHkHi4@+HZ zKRGOdV7_x$&tTAt8>z!?K0S(`toHY;#CHtqvthg>Q(vHlD1{o^*U#~nLhp5{_tLri zQgZ)8dig?pztjsnw0zT4udo>jEJqlKsduf);AIA-hOu{IC^RE zvy6lM8sqrn_wN=6GBhx~BnsY3ov)DYmkay_1pNes(i9Zk3kXDU7Siz-IP}%wetgXg zLtjRnA4EageFm|fwg1$h^So8@8eJIS7y9E@CFaY#>PxBl#dEJp&Tq)hm!;=dY9EZ_^w?jPIV zmu&AJ)Za_H_QD~1VT@ls^^qm~wdo@KuO3h?=Koyk7$+9QUoX|A^1xRFgK>X)`l|zP zX#vCm&9J9W0Hp4ss<~qS{nrT4ZRZz%jj+E)?U1(ZU!zOnHEq~ozkDFv|LcGL^xvQ* zIE3KQ|LdK8sY-s}~`jayE%a?tHWxuX;eEay5uN?nZ5bsxE z;^*}L8See^^^dPUEsaQsC*`%S0%JNEK#sJxK)th`{@oAUCeynGwIVDy{v@}|7}PL!9lOu{DZ zyz&zM^1r9;U%WSc<%JW^{NDAI1xx(-7mEDN>MJzCeed}G?)u8>YxC{lz3D6e1byYS zQ`aBa*lSSaU#WOti#NsNx3747;~M>MxE%gc@qEvR+Vm5TvyUJZ`;}uCfc1d?EeM=} z|NX}0^=r4w3-0%IyUY+2yf0l4>~HFJnIRXyGQYubu_XCh9TdL!?F|ZN$luhUNCN+r zqZc$q0x81b6p8-^$B1LUGWZXV@mpRN`=(2(7&=J{UIRTXuU_}uRq}J`{beKl^HG13 z0AG5?mpuB3MgIl=lq3I;XUJE$*yo-h_=5SBU@?;Bm<3CN6{g=DMSccbe21IIhv1G+ zqFFu@=dWE}Lnn}*bwv5c`Ik_{-`~IFABU8_?hW$|pu_&YfX)KrW){I>zbS72ept+J z3sTTH^BW!1Z_N+#``t7D4idU+?ms1<8J2>iPyNdT^jA*bw`uM>*7{9mzsc;^q#%v| zw9MvTiJMe}#Z2{ssY0{JsP{z94?3ScWDzj39xchXYPQapbQI0Q(e0 z5e!As4ENho^=P2BEHIWNfbxV>7zr34$zi`@%z|1_9JN?5B**>J{Of;m1^>qBys_@T zgJ1qPtoE;S|L2I}AE`HSe}BE{==nwX&-8DhXm}o|*}sJ*|3&^SR@lJX?&9z1k+_^@n!ftWyBW@^Do%11V`aG!+res=U?EbG{s=_|NQ2z=l=cOD*hr= zMew)#R?H3+-MFAJ@LwD!<{vZ*e-H#U|6norgCyriNN#?FqUJ{y*!&1+4S&%1{0RMZ zw+#Q8W5%*7TfiARh-v@XE*bE>df{DOfDo@neE;<2=z?NiM0M|SrhEqo!2bNpSHkPS zw}1J{*Z3FF%AcIxm%e@z|G$x&+~2dVL;cQTf*A}Tbqpg|24`3rNIvLP!{NU&3x>f- zjKV1bXMbD9y9L4iN|6l70NsG)IDFO(7Qd1#$I#&a367>|>Yt|S{Nw`uZL@h}b^jzy zN2zMahuW{{Iv<|=ik%C`@Yj45|CPEF`DV|0iN`N~Zx>b+U{UyjBWQq=DHcX?K^CY>98Lp{h%YdlBrzPaMdnwECK!gK7#0T{m;R?Q`JY_Dzi~Qm zT>cMW&mw;h_AHv3-FE0dltGJlGid#53|gOM5Brt@Jj{3VTxVGjzkBgcvTx%3O=tN-=`2N9#$D3>$1gLt`0Jm%K5V== zROgTXQ??3xHVTYG;hX0ndoS6;&!L%Im%@^rS%lY@$a(Kwm z{t%{^C#*)ZeaHx-)uT> z{+^$={hj%Hk_7uJ!;v%rI5I)eG`o26WTEi|1%tCVuv*w}EB_0HF~8zqcr=BxGz|n0 z@#H*1veW|SXdrKBhFkp8e1U#)1^>qByfM4qzb_EQk-r09Ad-f;n=$f(uEzZV3`%dl zKySL*A4^v&y=(X%zdSA4U;k8l(3t-8*Z=+h{Pj5m9zJ4n){Bmev@%;DH30kfh_$yFkuof-`pQ7oRQ+*Oqg#b z%s-Y1v-iN@{xIQ;GgJFxIr_eEq4vhq-Yks&b{0mxNv}Xr=)*srsqv*F{8iBMv!+Ih zdowk@nHvAiV;XUoSQuv)e<0rrihT>_c~e~8d@o-0y?{>j@1h@n`TssP`hJg$KZpm$ z6Yr$)Ka_riz46XB{pjCbKayZxsy~DgXJ%4=G&iv~CiMpJ{#XF-4;l&i5+3=gqW4uM z@za(dmVL7Xy;*{O2bLh_4|UnYF5Ym$8&3E`;e^XSAYs+Nfda(+p*(ZGbg6pdYj1q* zEz$Vzi>FcSABv|jZ#?bI@ceIYcs4Idc=i5ZMw%DS?pKwtuOgYBH_}jVMw&Mx&9{v- z&zJoJov+KN`q;^{VSISPti229>Dfr00&_lz%Xvp}S=GQMOURd=H~8E-64!<&?h5*VX_|z7k+lip6E8Ih88w_-r`+X=aui^w!94TsmB_}W!*P) z(<_koFKpZA2XcP=k~WuWmzHAMc3++z_KV{)KW~OW*m|FZ!aX&?e#O3MQ_RC&m2q0W zG_IiPUKm&zdR+x$t)5bUzt9nZJ^dp%79&UiAHL{#e#Z|Ewi~KC{q8oOlm5Pe@b$Bn zU#0(RO_9R3q~2*d7}cAp%;_iD;L21mII zKMhHno1NyoFVE%uSLbQH#uQ!}zLmDAUxZeb2HSA@dSbD^T%C-D{q+sgIf}25}8kQyG|$8OIpzQ%&`Qbohj&eB?$SzBlg0Gj;6q z2fzaP-nYKC)8N@D{zxjHRp@@+kz6SpN#(*FMsc1S6zRtu4Nj-i>6=q;5VTEMo|&|n!bO-dZ0`egFrO}Dk%>h zfBwt|UFA7}J$wFe_n`mf^CnLVfqj$+&mK3^$ESn+xFRpJzV|!xoTYrq)qr*pNn$xW#XSfG?~+4`F5g?Xp0|F_8lE@M zs&W>BV9HN0@AZuJ^( z)4wE!&)@j$d3i1U(DTZ1r~XibjV}Mt^Sbt9&mY&)56l1ngs%O*J-WBy2hjfd;)ADl l^Jaea5v}SzJ$LVD?mhSUPqZ$8Z2zx6{r^NTZ2BZA1pvA%Cky}p literal 0 HcmV?d00001 diff --git a/packages/mongodb-schema/docs/mongodb-schema_diagram.png b/packages/mongodb-schema/docs/mongodb-schema_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..9de892dee95b78197fc7f84e3b1b3a91c5a8d02e GIT binary patch literal 53480 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�L3=VakF#$W)skf51xzm0U#lmYPtCsY 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 literal 0 HcmV?d00001 diff --git a/packages/mongodb-schema/examples/fanclub.json b/packages/mongodb-schema/examples/fanclub.json new file mode 100644 index 000000000..aa44f8344 --- /dev/null +++ b/packages/mongodb-schema/examples/fanclub.json @@ -0,0 +1,100 @@ +{"_id":{"$oid":"50319491fe4dce143835c552"},"membership_status":"ACTIVE","name":"Ellie J Clarke","gender":"male","age":36,"phone_no":"+19786213180","last_login":{"$date":"2014-01-31T22:26:33.000Z"},"address":{"city":"El Paso, Texas","street":"133 Aloha Ave","postal_code":50017,"country":"USA","location":{"type":"Point","coordinates":[-106.3974968970265,31.79689833641156]}},"favorite_feature":"Auth","email":"corefinder88@hotmail.com"} +{"_id":{"$oid":"5031bb65fe4dce143635c960"},"membership_status":"ACTIVE","name":"Ava Z Robinson","gender":"female","age":22,"phone_no":"+451634929422","last_login":{"$date":"2015-05-12T02:39:24.000Z"},"address":{"city":"El Paso, Texas","street":"290 East George Rd","postal_code":"18851-1275","country":"USA","location":{"type":"Point","coordinates":[-106.49044778805387,31.7957361699594]}},"email":"bubbles51@gmail.com"} +{"_id":{"$oid":"5031ce4bfe4dce143735c6df"},"membership_status":"PENDING","name":"Jayden V Allen","gender":"male","age":14,"phone_no":"+16632418722","last_login":{"$date":"2012-09-14T14:44:21.000Z"},"address":{"city":"Dallas, Texas","street":"369 Interurban Pkwy","postal_code":60240,"country":"USA","location":{"type":"Point","coordinates":[-96.80626011661555,32.76331397593709]}},"email":"netfreak22@msn.com"} +{"_id":{"$oid":"5031f557fe4dce143835c9b0"},"membership_status":"INACTIVE","name":"Joshua Evans","gender":"female","age":40,"phone_no":{"$numberLong":"6451617244"},"last_login":{"$date":"2012-06-22T13:19:16.000Z"},"address":{"city":"Chula Vista, California","street":"208 West Boston St","postal_code":"88662","country":"Australia","location":{"type":"Point","coordinates":[-117.11388821760998,32.707530991493854]}},"favorite_feature":"Document Validation","email":"takeaway45@icloud.com"} +{"_id":{"$oid":"5031fd5ffe4dce143835c242"},"membership_status":"ACTIVE","name":"Landon Jackson","gender":"male","phone_no":"+459395024796","last_login":{"$date":"2011-02-17T20:04:57.000Z"},"address":{"city":"Lincoln, Nebraska","street":"370 South Irving St","postal_code":"61657-9116","country":"UK","location":{"type":"Point","coordinates":[-96.6486323045409,40.749246312984]}},"favorite_feature":"Wired Tiger","email":"veryberry15@yahoo.com"} +{"_id":{"$oid":"50326538fe4dce143635c611"},"membership_status":"ACTIVE","name":"Madison J Harris","gender":"female","age":49,"phone_no":"+12347596128","last_login":{"$date":"2014-02-16T14:31:07.000Z"},"address":{"city":"Bakersfield, California","street":"86 West Prospect Ave","postal_code":97086,"country":"USA","location":{"type":"Point","coordinates":[-119.07966704760815,35.36248673002892]}},"email":"cookie25@comcast.net"} +{"_id":{"$oid":"50328e08fe4dce143535c7a9"},"membership_status":"ACTIVE","name":"Sophia Cooper","gender":"female","age":45,"phone_no":{"$numberLong":"6451029082"},"last_login":{"$date":"2013-02-26T14:35:11.000Z"},"address":{"city":"El Paso, Texas","street":"250 Clyde St","postal_code":87682,"country":"USA","location":{"type":"Point","coordinates":[-106.45620334201676,31.792055786355498]}},"favorite_feature":"Replication","email":"fiddle7@hotmail.com"} +{"_id":{"$oid":"5032a2c2fe4dce143835c32d"},"membership_status":"ACTIVE","name":"Andrew Wilson","phone_no":"+11774153168","last_login":{"$date":"2011-03-07T16:13:20.000Z"},"address":{"city":"Mesa, Arizona","street":"327 Olympic St","postal_code":"23996","country":"USA","location":{"type":"Point","coordinates":[-111.78826676616428,33.42673545202268]}},"favorite_feature":"Text Search","email":"dandy22@hotmail.com"} +{"_id":{"$oid":"5032c9effe4dce143735c442"},"membership_status":"ACTIVE","name":"Lucy U Martin","gender":"male","age":12,"phone_no":"+498384871227","last_login":{"$date":"2011-08-22T16:52:13.000Z"},"address":{"city":"Orlando, Florida","street":"82 South Edmunds Rd","postal_code":69727,"country":"USA","location":{"type":"Point","coordinates":[-81.3859647858629,28.521979342926883]}},"favorite_feature":"Wired Tiger","email":"shortcircuit81@comcast.net"} +{"_id":{"$oid":"5032edb1fe4dce143635cac3"},"membership_status":"ACTIVE","name":"Addison James","gender":"male","phone_no":"+1109044312","last_login":{"$date":"2014-03-05T13:44:52.000Z"},"address":{"city":"Columbus, Ohio","street":"334 Woodland Rd","postal_code":"72727","country":"USA","location":{"type":"Point","coordinates":[-83.03214264765697,39.913457135337374]}},"email":"mindprobe79@gmail.com"} +{"_id":{"$oid":"50331c53fe4dce143635c90a"},"membership_status":"ACTIVE","name":"Jacob White","gender":"male","age":29,"phone_no":"+16742335892","last_login":{"$date":"2010-09-27T08:07:21.000Z"},"address":{"city":"Riverside, California","street":"152 Northeast Reny St","postal_code":"50337","country":"Germany","location":{"type":"Point","coordinates":[-117.36917639593297,33.95725787096601]}},"email":"bliss35@comcast.net"} +{"_id":{"$oid":"5033325dfe4dce143735c81d"},"membership_status":"ACTIVE","name":"Ella Harris","gender":"male","age":43,"phone_no":"+14751027659","last_login":{"$date":"2010-12-19T09:50:38.000Z"},"address":{"city":"Boston, Massachusetts","street":"228 Perimeter Ave","postal_code":"77790","country":"USA","location":{"type":"Point","coordinates":[-71.06248839321384,42.36517500946935]}},"favorite_feature":"Document Validation","email":"sugarycake76@hotmail.com"} +{"_id":{"$oid":"50337271fe4dce143835c529"},"membership_status":"ACTIVE","name":"Sebastian C King","gender":"male","phone_no":"+18729615410","last_login":{"$date":"2014-04-28T09:51:46.000Z"},"address":{"city":"Charlotte, North Carolina","street":"296 Northeast Ring St","postal_code":39929,"country":"USA","location":{"type":"Point","coordinates":[-80.84749920829343,35.26558124444622]}},"favorite_feature":"Document Validation","email":"mencity14@verizon.net"} +{"_id":{"$oid":"5033ac9afe4dce143835c9ef"},"membership_status":"ACTIVE","name":"Zoey A James","phone_no":"+12275857418","last_login":{"$date":"2011-10-21T23:20:05.000Z"},"address":{"city":"Wichita, Kansas","street":"240 East Valley St","postal_code":"82275","country":"USA","location":{"type":"Point","coordinates":[-97.35401816658857,37.679287359830084]}},"favorite_feature":"Partial Indexes","email":"cybergypsy9@gmx.net"} +{"_id":{"$oid":"50341db4fe4dce143735c833"},"membership_status":"PENDING","name":"Jackson B Bell","gender":"female","phone_no":"+19688538381","last_login":{"$date":"2013-05-20T17:00:55.000Z"},"address":{"city":"Nashville-Davidson, Tennessee","street":"197 Birch St","postal_code":"82254","country":"USA","location":{"type":"Point","coordinates":[-86.73538620987506,36.151079202231024]}},"favorite_feature":"Aggregation","email":"mr.america12@yahoo.com"} +{"_id":{"$oid":"5034782ffe4dce143635c9c7"},"membership_status":"PENDING","name":"Ethan James","gender":"female","age":43,"phone_no":"+13103764856","last_login":{"$date":"2015-05-11T18:08:36.000Z"},"address":{"city":"Sacramento, California","street":"61 Southwest Dakota Ave","postal_code":"80362","country":"USA","location":{"type":"Point","coordinates":[-121.51720950611428,38.58973166161231]}},"email":"hypnosis57@comcast.net"} +{"_id":{"$oid":"5034d1d6fe4dce143735c1c4"},"membership_status":"INACTIVE","name":"Ryan Lee","gender":"male","phone_no":"+19345986970","last_login":{"$date":"2013-10-24T14:50:59.000Z"},"address":{"city":"Scottsdale, Arizona","street":"167 South Carver St","postal_code":"78105","country":"USA","location":{"type":"Point","coordinates":[-111.95440344669727,33.510098974798275]}},"favorite_feature":"Aggregation","email":"sugarycake98@msn.com"} +{"_id":{"$oid":"5034ef31fe4dce143535c313"},"membership_status":"ACTIVE","name":"Mason C Williams","gender":"male","age":49,"phone_no":"+611382078320","last_login":{"$date":"2011-12-06T15:08:12.000Z"},"address":{"city":"Memphis, Tennessee","street":"250 Northeast Larchmount St","postal_code":"55813","country":"Australia","location":{"type":"Point","coordinates":[-90.0590125637064,35.13977080908803]}},"favorite_feature":"Auth","email":"cyberking8@aim.com"} +{"_id":{"$oid":"50350163fe4dce143835ca46"},"membership_status":"PENDING","name":"Ava Watson","gender":"male","age":31,"phone_no":"+498289859715","last_login":{"$date":"2014-05-14T14:10:05.000Z"},"address":{"city":"San Diego, California","street":"156 Aloha St","postal_code":"87651","country":"USA","location":{"type":"Point","coordinates":[-117.15234182944589,32.70511752957363]}},"email":"dixie0@hotmail.com"} +{"_id":{"$oid":"503517c6fe4dce143635c2cf"},"membership_status":"PENDING","name":"Hannah Harris","gender":"female","age":35,"phone_no":"+16491667120","last_login":{"$date":"2015-02-02T08:47:46.000Z"},"address":{"city":"St. Paul, Minnesota","street":"116 Christensen Ave","postal_code":70051,"country":"USA","location":{"type":"Point","coordinates":[-93.11363402069591,44.93880912265563]}},"favorite_feature":"Document Validation","email":"jaycee87@gmail.com"} +{"_id":{"$oid":"503525aefe4dce143735c628"},"membership_status":"ACTIVE","name":"Gabriel Lewis","age":36,"phone_no":{"$numberLong":"5705326570"},"last_login":{"$date":"2013-04-01T19:41:53.000Z"},"address":{"city":"Fresno, California","street":"102 Northwest Spring Fork Rd","postal_code":"48866","country":"USA","location":{"type":"Point","coordinates":[-119.81614942751808,36.72190998438775]}},"email":"coolstrawberry18@gmx.net"} +{"_id":{"$oid":"50353108fe4dce143835c73a"},"membership_status":"PENDING","name":"Lucas T Scott","gender":"male","phone_no":{"$numberLong":"4638336714"},"last_login":{"$date":"2012-06-17T20:02:48.000Z"},"address":{"city":"Wichita, Kansas","street":"213 Union Oil Company Ln","postal_code":"82023","country":"USA","location":{"type":"Point","coordinates":[-97.34389510412187,37.694450426102286]}},"favorite_feature":"Document Validation","email":"broomspun45@icloud.com"} +{"_id":{"$oid":"50354f8afe4dce143535c72b"},"membership_status":"PENDING","name":"Ryan C Clark","gender":"female","age":47,"phone_no":"+17581816982","last_login":{"$date":"2013-06-12T04:27:02.000Z"},"address":{"city":"Kansas City, Missouri","street":"158 Northeast Ohio Ave","postal_code":22179,"country":"USA","location":{"type":"Point","coordinates":[-94.61018096397198,39.02076719209666]}},"favorite_feature":"Document Validation","email":"littletrout73@icloud.com"} +{"_id":{"$oid":"5035aabbfe4dce143735c3fe"},"membership_status":"ACTIVE","name":"Elijah Edwards","age":33,"phone_no":{"$numberLong":"9508380915"},"last_login":{"$date":"2012-09-25T21:16:35.000Z"},"address":{"city":"El Paso, Texas","street":"29 Northwest Elford Ln","postal_code":"72220","country":"USA","location":{"type":"Point","coordinates":[-106.38527383924198,31.771914753348636]}},"email":"mencity29@aim.com"} +{"_id":{"$oid":"5035add2fe4dce143635c4a5"},"membership_status":"ACTIVE","name":"Audrey O Phillips","gender":"female","phone_no":"+14238559669","last_login":{"$date":"2012-10-23T07:56:08.000Z"},"address":{"city":"Austin, Texas","street":"317 Homeview St","postal_code":"19525","country":"UK","location":{"type":"Point","coordinates":[-97.72969424683606,30.267998395327737]}},"email":"born-confused35@aim.com"} +{"_id":{"$oid":"5035b81cfe4dce143635c249"},"membership_status":"ACTIVE","name":"Oliver O Wilson","gender":"male","age":44,"phone_no":1444775294,"last_login":{"$date":"2014-07-08T03:18:58.000Z"},"address":{"city":"Tucson, Arizona","street":"148 Mallard Ln","postal_code":"63394","country":"UK","location":{"type":"Point","coordinates":[-110.92346399038425,32.24217493236621]}},"email":"cooldora15@verizon.net"} +{"_id":{"$oid":"5035bf1efe4dce143635c9a3"},"membership_status":"PENDING","name":"Samuel B Thompson","gender":"female","phone_no":"+14240959525","last_login":{"$date":"2013-04-23T22:18:46.000Z"},"address":{"city":"Omaha, Nebraska","street":"121 Tolt Rd","postal_code":18920,"country":"UK","location":{"type":"Point","coordinates":[-95.9882005313852,41.243236002762096]}},"favorite_feature":"Wired Tiger","email":"cookie93@icloud.com"} +{"_id":{"$oid":"503614c4fe4dce143835c347"},"membership_status":"ACTIVE","name":"Nora P Turner","gender":"female","phone_no":"+15238242224","last_login":{"$date":"2013-08-21T02:32:30.000Z"},"address":{"city":"New Orleans, Louisiana","street":"301 East Louisa Ln","postal_code":"98397","country":"USA","location":{"type":"Point","coordinates":[-90.1258060814679,29.949670711760216]}},"email":"gozmit56@gmail.com"} +{"_id":{"$oid":"50362919fe4dce143635c18a"},"membership_status":"ACTIVE","name":"Oliver Williams","gender":"female","age":34,"phone_no":"+13911696939","last_login":{"$date":"2011-06-30T00:49:33.000Z"},"address":{"city":"Colorado Springs, Colorado","street":"28 Colonial St","postal_code":"64215","country":"USA","location":{"type":"Point","coordinates":[-104.84322652446603,38.830924227214595]}},"email":"techbro68@comcast.net"} +{"_id":{"$oid":"50363146fe4dce143535c24a"},"membership_status":"ACTIVE","name":"Aiden F Watson","gender":"female","age":23,"phone_no":"+11798558233","last_login":{"$date":"2010-01-23T00:14:28.000Z"},"address":{"city":"Baton Rouge, Louisiana","street":"240 South Nebraska Rd","postal_code":"92026","country":"USA","location":{"type":"Point","coordinates":[-91.16401489428318,30.455760139092977]}},"favorite_feature":"Document Validation","email":"beloved27@yahoo.com"} +{"_id":{"$oid":"503631f2fe4dce143735c66c"},"membership_status":"ACTIVE","name":"Caleb Z Robinson","age":33,"phone_no":"+18309799235","last_login":{"$date":"2014-11-11T13:51:03.000Z"},"address":{"city":"Toledo, Ohio","street":"5 South Nebraska Rd","postal_code":"50946","country":"France","location":{"type":"Point","coordinates":[-83.55678741720347,41.68113909282523]}},"email":"brownie34@msn.com"} +{"_id":{"$oid":"5036425afe4dce143535c32e"},"membership_status":"ACTIVE","name":"Avery Hall","gender":"female","phone_no":"+447393926788","last_login":{"$date":"2012-02-01T10:31:27.000Z"},"address":{"city":"Tampa, Florida","street":"395 Whitman Rd","postal_code":20322,"country":"France","location":{"type":"Point","coordinates":[-82.44397018158193,27.907286098758533]}},"favorite_feature":"Document Validation","email":"braindead97@hotmail.com"} +{"_id":{"$oid":"503678c6fe4dce143635c79b"},"membership_status":"PENDING","name":"Madison Clark","phone_no":"+1909542687","last_login":{"$date":"2013-10-03T05:45:25.000Z"},"address":{"city":"Lincoln, Nebraska","street":"212 Erben Rd","postal_code":"34224","country":"USA","location":{"type":"Point","coordinates":[-96.72098855282341,40.79275323018288]}},"email":"couldnt_find_good_name79@yahoo.com"} +{"_id":{"$oid":"5036fc0afe4dce143535c891"},"membership_status":"ACTIVE","name":"James Young","gender":"female","age":52,"phone_no":"+18770127752","last_login":{"$date":"2012-12-14T08:15:53.000Z"},"address":{"city":"Riverside, California","street":"374 Southwest Sullivan Ln","postal_code":"46994","country":"USA","location":{"type":"Point","coordinates":[-117.38986103778586,33.92300743235786]}},"favorite_feature":"Partial Indexes","email":"fiddle29@verizon.net"} +{"_id":{"$oid":"50370aaefe4dce143735c679"},"membership_status":"INACTIVE","name":"Logan Phillips","gender":"male","age":23,"phone_no":1272641335,"last_login":{"$date":"2011-02-16T02:11:10.000Z"},"address":{"city":"Oklahoma City, Oklahoma","street":"276 Crescent Lake Rd","postal_code":"48963","country":"USA","location":{"type":"Point","coordinates":[-97.47815616033876,35.41129245670654]}},"favorite_feature":"Aggregation","email":"takeaway34@verizon.net"} +{"_id":{"$oid":"50372c54fe4dce143835ca20"},"membership_status":"ACTIVE","name":"Ethan G Turner","gender":"male","age":37,"phone_no":"+44654308202","last_login":{"$date":"2014-07-04T02:37:43.000Z"},"address":{"city":"Austin, Texas","street":"325 Alexander Rd","postal_code":"71696","country":"USA","location":{"type":"Point","coordinates":[-97.7120214873177,30.21646994084725]}},"favorite_feature":"Partial Indexes","email":"lilokie78@aim.com"} +{"_id":{"$oid":"503743edfe4dce143635ca6f"},"membership_status":"ACTIVE","name":"Emma P Lewis","gender":"female","age":25,"phone_no":"+13612660749","last_login":{"$date":"2014-10-03T11:02:11.000Z"},"address":{"city":"Chesapeake, Virginia","street":"63 Southwest Jacobsen Rd","postal_code":31117,"country":"USA","location":{"type":"Point","coordinates":[-76.25802025410239,36.77630401530377]}},"favorite_feature":"Document Validation","email":"hockeywain66@comcast.net"} +{"_id":{"$oid":"50374ee3fe4dce143835c369"},"membership_status":"ACTIVE","name":"Logan Anderson","gender":"female","phone_no":{"$numberLong":"7886157051"},"last_login":{"$date":"2010-12-27T02:23:02.000Z"},"address":{"city":"Chicago, Illinois","street":"72 Northeast Mary Lou St","postal_code":"78647-4033","country":"USA","location":{"type":"Point","coordinates":[-87.64403496388532,41.917318177067]}},"favorite_feature":"Auth","email":"awesomegirl90@gmx.net"} +{"_id":{"$oid":"503766ccfe4dce143535c716"},"membership_status":"ACTIVE","name":"Sebastian Taylor","gender":"female","age":30,"phone_no":"+11264294482","last_login":{"$date":"2013-09-19T12:30:54.000Z"},"address":{"city":"El Paso, Texas","street":"107 Southwest Francis Rd","postal_code":"97603","country":"UK","location":{"type":"Point","coordinates":[-106.49261738244712,31.805160294856076]}},"favorite_feature":"Document Validation","email":"gozmit91@icloud.com"} +{"_id":{"$oid":"50378103fe4dce143735c939"},"membership_status":"PENDING","name":"Owen U Lewis","gender":"female","age":44,"phone_no":"+445622359175","last_login":{"$date":"2013-12-03T04:48:40.000Z"},"address":{"city":"Reno, Nevada","street":"247 Southwest Normandy Pkwy","postal_code":15907,"country":"USA","location":{"type":"Point","coordinates":[-119.81237626153762,39.57997164096322]}},"favorite_feature":"Sharding","email":"cybergypsy26@hotmail.com"} +{"_id":{"$oid":"5037ab4cfe4dce143535c20b"},"membership_status":"PENDING","name":"Matthew L Hughes","gender":"male","age":39,"phone_no":"+11297282707","last_login":{"$date":"2010-08-15T02:36:45.000Z"},"address":{"city":"Arlington, Texas","street":"257 Northeast White Horse Rd","postal_code":"36444","country":"USA","location":{"type":"Point","coordinates":[-97.08165811415708,32.7476245290993]}},"favorite_feature":"Wired Tiger","email":"awesomegirl68@yahoo.com"} +{"_id":{"$oid":"5037c02cfe4dce143835c2b0"},"membership_status":"ACTIVE","name":"Dylan G Davis","phone_no":"+451104340553","last_login":{"$date":"2010-04-02T16:46:57.000Z"},"address":{"city":"Milwaukee, Wisconsin","street":"188 Terrace St","postal_code":"59470","country":"USA","location":{"type":"Point","coordinates":[-87.9166769189508,43.0732589209909]}},"favorite_feature":"Aggregation","email":"coolwhip11@gmx.net"} +{"_id":{"$oid":"5038287dfe4dce143635c82c"},"membership_status":"PENDING","name":"Landon K Thompson","gender":"female","phone_no":"+18052943426","last_login":{"$date":"2010-05-10T12:04:46.000Z"},"address":{"city":"Laredo, Texas","street":"90 South Rainbow Rd","postal_code":92861,"country":"UK","location":{"type":"Point","coordinates":[-99.48901986890544,27.53698988541721]}},"favorite_feature":"Document Validation","email":"funkymoney32@yahoo.com"} +{"_id":{"$oid":"5038419efe4dce143735c7e0"},"membership_status":"PENDING","name":"Scarlett N Anderson","gender":"male","age":42,"phone_no":"+19824671192","last_login":{"$date":"2010-09-19T14:18:20.000Z"},"address":{"city":"Tulsa, Oklahoma","street":"228 Tye River Ave","postal_code":12337,"country":"USA","location":{"type":"Point","coordinates":[-95.9594459276981,36.173443213456366]}},"email":"neurotic33@aim.com"} +{"_id":{"$oid":"5038662ffe4dce143835c319"},"membership_status":"ACTIVE","name":"Emma Z Harris","gender":"male","phone_no":"+15102966031","last_login":{"$date":"2013-06-17T03:56:30.000Z"},"address":{"city":"San Francisco, California","street":"195 Baker Rd","postal_code":"46954","country":"USA","location":{"type":"Point","coordinates":[-122.45504480822119,37.78345435889993]}},"email":"coolstrawberry57@icloud.com"} +{"_id":{"$oid":"503872d4fe4dce143735c155"},"membership_status":"ACTIVE","name":"Aubrey Z Lewis","gender":"female","phone_no":"+12931558514","last_login":{"$date":"2013-04-29T04:44:29.000Z"},"address":{"city":"Denver, Colorado","street":"197 Northeast Ocean Rd","postal_code":"32469","country":"USA","location":{"type":"Point","coordinates":[-105.04661760670464,39.73991078943252]}},"favorite_feature":"Auth","email":"heavenlybeliever67@msn.com"} +{"_id":{"$oid":"5038758dfe4dce143835c1b5"},"membership_status":"ACTIVE","name":"James C Johnson","gender":"female","phone_no":"+445979910409","last_login":{"$date":"2013-08-05T07:36:50.000Z"},"address":{"city":"Chesapeake, Virginia","street":"115 Northwest Firwood Ln","postal_code":"98778","country":"USA","location":{"type":"Point","coordinates":[-76.3289057935534,36.767456583381815]}},"email":"bubbles78@gmx.net"} +{"_id":{"$oid":"503883fafe4dce143635c60e"},"membership_status":"ACTIVE","name":"Grace B Robinson","gender":"female","phone_no":"+12018846177","last_login":{"$date":"2013-01-28T23:18:55.000Z"},"address":{"city":"Boston, Massachusetts","street":"243 Andover Ln","postal_code":"91337","country":"USA","location":{"type":"Point","coordinates":[-71.00580541060702,42.273106022754966]}},"favorite_feature":"Wired Tiger","email":"override56@icloud.com"} +{"_id":{"$oid":"5038af20fe4dce143535c4d0"},"membership_status":"ACTIVE","name":"Addison K Roberts","gender":"male","phone_no":"+444202064667","last_login":{"$date":"2014-07-05T05:38:41.000Z"},"address":{"city":"Lexington-Fayette, Kentucky","street":"328 Hanna Park Rd","postal_code":"38847","country":"Australia","location":{"type":"Point","coordinates":[-84.51005328061954,38.033158533515454]}},"email":"crashtest8@msn.com"} +{"_id":{"$oid":"5038cd94fe4dce143535c2f7"},"membership_status":"ACTIVE","name":"Jayden Brown","gender":"female","age":39,"phone_no":"+13005615088","last_login":{"$date":"2011-07-28T18:21:31.000Z"},"address":{"city":"Lincoln, Nebraska","street":"173 Skagit Ave","postal_code":59448,"country":"USA","location":{"type":"Point","coordinates":[-96.68606033674155,40.823391244737735]}},"favorite_feature":"Partial Indexes","email":"bullfrog67@gmail.com"} +{"_id":{"$oid":"5038f20ffe4dce143735c5f5"},"membership_status":"ACTIVE","name":"Harper M Hall","age":36,"phone_no":{"$numberLong":"6012853977"},"last_login":{"$date":"2013-06-28T09:55:48.000Z"},"address":{"city":"New York, New York","street":"397 Homeview St","postal_code":"77008-7338","country":"USA","location":{"type":"Point","coordinates":[-74.01711966985118,40.69069738086477]}},"favorite_feature":"Sharding","email":"angelwonderland13@comcast.net"} +{"_id":{"$oid":"5038f3d4fe4dce143635c8e9"},"membership_status":"PENDING","name":"Jacob C Allen","gender":"female","phone_no":"+16290756822","last_login":{"$date":"2010-04-13T21:44:47.000Z"},"address":{"city":"El Paso, Texas","street":"101 Fern Hollow Ave","postal_code":"52129","country":"USA","location":{"type":"Point","coordinates":[-106.44942793972183,31.72755655037713]}},"favorite_feature":"Sharding","email":"chiquitita85@gmail.com"} +{"_id":{"$oid":"503911cbfe4dce143535c556"},"membership_status":"ACTIVE","name":"Carter H Green","gender":"male","age":27,"phone_no":{"$numberLong":"4146224337"},"last_login":{"$date":"2011-03-08T09:34:19.000Z"},"address":{"city":"Baton Rouge, Louisiana","street":"283 Hedlund Rd","postal_code":"25371-2506","country":"USA","location":{"type":"Point","coordinates":[-91.1410148115081,30.4462164714689]}},"favorite_feature":"Sharding","email":"brownie79@comcast.net"} +{"_id":{"$oid":"503986e4fe4dce143835c15b"},"membership_status":"ACTIVE","name":"Matthew P Scott","gender":"male","age":29,"phone_no":"+445428835071","last_login":{"$date":"2013-04-23T16:14:30.000Z"},"address":{"city":"Fort Wayne, Indiana","street":"193 South Tillicum Ln","postal_code":"26305","country":"USA","location":{"type":"Point","coordinates":[-85.14886341838589,41.05923884850483]}},"email":"delicious72@gmx.net"} +{"_id":{"$oid":"50399796fe4dce143835c3fd"},"membership_status":"ACTIVE","name":"Ryan C Thompson","age":40,"phone_no":"+17725961350","last_login":{"$date":"2012-07-21T02:42:21.000Z"},"address":{"city":"Chicago, Illinois","street":"8 Woodlake St","postal_code":84957,"country":"Germany","location":{"type":"Point","coordinates":[-87.5908109568315,41.90314381173272]}},"favorite_feature":"Sharding","email":"happinesskingdom2@comcast.net"} +{"_id":{"$oid":"5039e165fe4dce143735ca01"},"membership_status":"ACTIVE","name":"Samuel Hall","gender":"male","phone_no":"+613118264437","last_login":{"$date":"2011-03-17T19:33:41.000Z"},"address":{"city":"Jacksonville, Florida","street":"398 South Redwing St","postal_code":"24576","country":"USA","location":{"type":"Point","coordinates":[-81.63928472883148,30.319041086004702]}},"favorite_feature":"Wired Tiger","email":"beauty36@comcast.net"} +{"_id":{"$oid":"5039f325fe4dce143635caf3"},"membership_status":"ACTIVE","name":"Aria B James","gender":"female","age":42,"phone_no":"+448099839354","last_login":{"$date":"2015-05-22T16:08:54.000Z"},"address":{"city":"Boston, Massachusetts","street":"329 Northeast Oddfellows St","postal_code":"81320-4562","country":"USA","location":{"type":"Point","coordinates":[-71.05506508831115,42.388184299141216]}},"email":"override73@comcast.net"} +{"_id":{"$oid":"5039f3a6fe4dce143835c6b1"},"membership_status":"PENDING","name":"Scarlett Robinson","phone_no":{"$numberLong":"4322399992"},"last_login":{"$date":"2010-12-24T15:15:18.000Z"},"address":{"city":"Las Vegas, Nevada","street":"184 West Barrett Ln","postal_code":"11790-4943","country":"UK","location":{"type":"Point","coordinates":[-115.18518799507171,36.15646878805473]}},"favorite_feature":"Aggregation","email":"max78@gmx.net"} +{"_id":{"$oid":"503a0c42fe4dce143835cad0"},"membership_status":"PENDING","name":"Owen M Ward","gender":"male","phone_no":{"$numberLong":"2613305652"},"last_login":{"$date":"2012-07-20T06:12:21.000Z"},"address":{"city":"Modesto, California","street":"148 Oregon Rd","postal_code":"23280","country":"USA","location":{"type":"Point","coordinates":[-121.00074370342853,37.661484144467074]}},"favorite_feature":"Document Validation","email":"born2pizza89@verizon.net"} +{"_id":{"$oid":"503a2d71fe4dce143535c968"},"membership_status":"ACTIVE","name":"Daniel Jackson","gender":"female","phone_no":"+1231970440","last_login":{"$date":"2013-04-01T17:09:31.000Z"},"address":{"city":"Plano, Texas","street":"166 Skagit Rd","postal_code":61737,"country":"USA","location":{"type":"Point","coordinates":[-96.67194563553288,33.051754178837186]}},"email":"buttercup78@hotmail.com"} +{"_id":{"$oid":"503a3992fe4dce143635c23c"},"membership_status":"ACTIVE","name":"Lillian N Morgan","gender":"male","age":32,"phone_no":"+444233082982","last_login":{"$date":"2013-10-25T23:40:19.000Z"},"address":{"city":"Chula Vista, California","street":"305 Southwest Orchard Ave","postal_code":"33875","country":"USA","location":{"type":"Point","coordinates":[-117.08959008455346,32.637427567346386]}},"favorite_feature":"Wired Tiger","email":"wokie46@gmx.net"} +{"_id":{"$oid":"503a5ecafe4dce143835c173"},"membership_status":"ACTIVE","name":"Lillian F Watson","gender":"male","age":36,"phone_no":"+18106420364","last_login":{"$date":"2010-08-21T16:51:45.000Z"},"address":{"city":"Reno, Nevada","street":"302 Northeast Miller St","postal_code":"12985","country":"USA","location":{"type":"Point","coordinates":[-119.83369942991055,39.49746754215054]}},"favorite_feature":"Partial Indexes","email":"lamb48@verizon.net"} +{"_id":{"$oid":"503a6039fe4dce143635c6f7"},"membership_status":"ACTIVE","name":"Logan E Morgan","gender":"female","age":30,"phone_no":"+14717544344","last_login":{"$date":"2010-05-15T20:32:02.000Z"},"address":{"city":"Henderson, Nevada","street":"82 Northwest Sammamish St","postal_code":24722,"country":"USA","location":{"type":"Point","coordinates":[-114.99377231450256,36.061844769494364]}},"favorite_feature":"Partial Indexes","email":"cyberwarrior31@hotmail.com"} +{"_id":{"$oid":"503a6992fe4dce143535c92b"},"membership_status":"ACTIVE","name":"Leah U Allen","gender":"male","age":32,"phone_no":"+15032549920","last_login":{"$date":"2013-07-21T22:14:04.000Z"},"address":{"city":"Portland, Oregon","street":"177 Robin Hood Rd","postal_code":"57759","country":"USA","location":{"type":"Point","coordinates":[-122.66916448512227,45.557548287657134]}},"favorite_feature":"Document Validation","email":"braindead24@verizon.net"} +{"_id":{"$oid":"503aa5e9fe4dce143835c9b2"},"membership_status":"ACTIVE","name":"Amelie D Mitchell","gender":"female","age":46,"phone_no":1283342351,"last_login":{"$date":"2011-08-30T14:30:37.000Z"},"address":{"city":"Washington, District of Columbia","street":"64 Prospect Rd","postal_code":"21301","country":"France","location":{"type":"Point","coordinates":[-76.9947302232287,38.884057570681485]}},"email":"exotica10@msn.com"} +{"_id":{"$oid":"503ac400fe4dce143735c1d6"},"membership_status":"ACTIVE","name":"Mia B Morgan","gender":"female","phone_no":"+14530966355","last_login":{"$date":"2013-04-09T16:22:31.000Z"},"address":{"city":"Sacramento, California","street":"106 East Commercial Rd","postal_code":"86084","country":"USA","location":{"type":"Point","coordinates":[-121.43642684095471,38.53404730943275]}},"favorite_feature":"Document Validation","email":"hangman40@gmx.net"} +{"_id":{"$oid":"503ad4fffe4dce143535ca4b"},"membership_status":"ACTIVE","name":"Scarlett M Phillips","gender":"female","age":37,"phone_no":"+497348601728","last_login":{"$date":"2010-09-18T23:50:12.000Z"},"address":{"city":"Tampa, Florida","street":"353 Entwistle St","postal_code":"33035","country":"USA","location":{"type":"Point","coordinates":[-82.49890668718594,27.875646539283746]}},"email":"cheekygirl84@comcast.net"} +{"_id":{"$oid":"503b01b7fe4dce143835c66d"},"membership_status":"PENDING","name":"Avery H Hughes","gender":"female","age":34,"phone_no":"+18516876471","last_login":{"$date":"2011-02-06T00:40:37.000Z"},"address":{"city":"Sacramento, California","street":"4 South Bradford St","postal_code":49836,"country":"USA","location":{"type":"Point","coordinates":[-121.49176171143955,38.58426237703769]}},"email":"dixie29@verizon.net"} +{"_id":{"$oid":"503b0ad2fe4dce143835c79e"},"membership_status":"ACTIVE","name":"Harper Smith","gender":"male","age":17,"phone_no":{"$numberLong":"8825640869"},"last_login":{"$date":"2014-04-17T01:41:13.000Z"},"address":{"city":"Stockton, California","street":"388 Northeast Meadowmeer St","postal_code":"67876","country":"USA","location":{"type":"Point","coordinates":[-121.30857723467344,37.97817618770525]}},"email":"coolguy48@msn.com"} +{"_id":{"$oid":"503b12f7fe4dce143535c318"},"membership_status":"ACTIVE","name":"Avery Martin","gender":"female","age":43,"phone_no":"+16700326504","last_login":{"$date":"2013-07-02T02:50:57.000Z"},"address":{"city":"Birmingham, Alabama","street":"383 Southwest Lisabuela St","postal_code":11988,"country":"USA","location":{"type":"Point","coordinates":[-86.86582181058897,33.487512379704626]}},"favorite_feature":"Sharding","email":"brads62@yahoo.com"} +{"_id":{"$oid":"503b1bb1fe4dce143535c8e3"},"membership_status":"ACTIVE","name":"Carter K James","gender":"female","age":46,"phone_no":"+615078400838","last_login":{"$date":"2010-09-01T10:55:39.000Z"},"address":{"city":"Boston, Massachusetts","street":"397 South Glacier St","postal_code":"66192","country":"USA","location":{"type":"Point","coordinates":[-71.10961286815105,42.374431032294524]}},"email":"beautiful64@comcast.net"} +{"_id":{"$oid":"503b290ffe4dce143835c147"},"membership_status":"ACTIVE","name":"Ryan Patel","gender":"male","phone_no":"+17438917865","last_login":{"$date":"2014-11-20T12:00:33.000Z"},"address":{"city":"Tulsa, Oklahoma","street":"361 Fairweather St","postal_code":"54522","country":"USA","location":{"type":"Point","coordinates":[-95.96451471507615,36.14748935864255]}},"favorite_feature":"Wired Tiger","email":"sugarypie57@verizon.net"} +{"_id":{"$oid":"503b4be4fe4dce143835cacc"},"membership_status":"PENDING","name":"Ellie Thompson","gender":"female","phone_no":"+17518179206","last_login":{"$date":"2012-05-05T08:03:06.000Z"},"address":{"city":"Las Vegas, Nevada","street":"88 Main Blvd","postal_code":"55245","country":"UK","location":{"type":"Point","coordinates":[-115.05775753288418,36.25014524785538]}},"email":"corefinder3@yahoo.com"} +{"_id":{"$oid":"503b4cb5fe4dce143535c3c5"},"membership_status":"ACTIVE","name":"Victoria B Wilson","gender":"male","age":51,"phone_no":"+14108799192","last_login":{"$date":"2011-06-06T08:18:39.000Z"},"address":{"city":"Columbus, Ohio","street":"198 South Taft St","postal_code":"83136","country":"USA","location":{"type":"Point","coordinates":[-82.9785189467614,39.92755073919848]}},"email":"fiddle59@hotmail.com"} +{"_id":{"$oid":"503b5c05fe4dce143535c89c"},"membership_status":"ACTIVE","name":"Sofia Scott","gender":"female","age":30,"phone_no":"+17566267862","last_login":{"$date":"2015-04-08T18:39:16.000Z"},"address":{"city":"Las Vegas, Nevada","street":"345 Driftwood St","postal_code":"52317","country":"USA","location":{"type":"Point","coordinates":[-115.10721160033528,36.16309462623908]}},"favorite_feature":"Text Search","email":"exotica92@verizon.net"} +{"_id":{"$oid":"503ba46ffe4dce143835c7fd"},"membership_status":"PENDING","name":"David Wood","gender":"male","age":35,"phone_no":"+452641424347","last_login":{"$date":"2013-08-17T01:14:50.000Z"},"address":{"city":"Boston, Massachusetts","street":"51 South Bayview Pkwy","postal_code":"97362","country":"USA","location":{"type":"Point","coordinates":[-71.11036616565663,42.325196359227974]}},"email":"entertain-me32@icloud.com"} +{"_id":{"$oid":"503bb442fe4dce143735c517"},"membership_status":"PENDING","name":"Sofia C Lewis","gender":"male","age":16,"phone_no":"+15615547175","last_login":{"$date":"2010-07-21T10:13:14.000Z"},"address":{"city":"Lubbock, Texas","street":"258 Northeast Monsaas St","postal_code":85936,"country":"USA","location":{"type":"Point","coordinates":[-101.87915639556758,33.56066575051059]}},"favorite_feature":"Document Validation","email":"cutie4@hotmail.com"} +{"_id":{"$oid":"503bb4edfe4dce143635c93b"},"membership_status":"ACTIVE","name":"Aubrey Jackson","gender":"female","age":39,"phone_no":"+443466760466","last_login":{"$date":"2012-05-04T18:46:38.000Z"},"address":{"city":"Stockton, California","street":"206 Southwest Monroe Ln","postal_code":"46020","country":"USA","location":{"type":"Point","coordinates":[-121.26483150067776,38.016984803522384]}},"favorite_feature":"Document Validation","email":"cutie31@verizon.net"} +{"_id":{"$oid":"503c0411fe4dce143635cae6"},"membership_status":"ACTIVE","name":"Isabella F Edwards","gender":"female","phone_no":"+446173137697","last_login":{"$date":"2013-08-17T18:45:38.000Z"},"address":{"city":"Baton Rouge, Louisiana","street":"374 Valley View St","postal_code":"68015","country":"France","location":{"type":"Point","coordinates":[-91.13453114306226,30.416559952695952]}},"favorite_feature":"Wired Tiger","email":"delicious45@aim.com"} +{"_id":{"$oid":"503c0c5ffe4dce143535c836"},"membership_status":"PENDING","name":"Sebastian Davies","gender":"male","phone_no":"+12660424548","last_login":{"$date":"2010-05-31T17:50:56.000Z"},"address":{"city":"Plano, Texas","street":"174 West Thomas Pkwy","postal_code":"70038","country":"USA","location":{"type":"Point","coordinates":[-96.6953271803223,33.032081774581656]}},"favorite_feature":"Document Validation","email":"happysky25@comcast.net"} +{"_id":{"$oid":"503c2376fe4dce143635c28a"},"membership_status":"ACTIVE","name":"Amelie J Robinson","gender":"female","age":40,"phone_no":"+19744516860","last_login":{"$date":"2011-07-22T12:30:43.000Z"},"address":{"city":"Huntington, New York","street":"338 Observation Rd","postal_code":"11950","country":"USA","location":{"type":"Point","coordinates":[-73.4446279457308,40.89674015263909]}},"favorite_feature":"Aggregation","email":"blossom86@gmail.com"} +{"_id":{"$oid":"503c35d0fe4dce143535c340"},"membership_status":"ACTIVE","name":"Luke W Allen","gender":"male","phone_no":"+496367386048","last_login":{"$date":"2011-02-13T02:36:54.000Z"},"address":{"city":"Omaha, Nebraska","street":"172 Northwest Far Country Ln","postal_code":"28762","country":"USA","location":{"type":"Point","coordinates":[-95.99510478645988,41.27130441732978]}},"favorite_feature":"Partial Indexes","email":"manicpsycho28@yahoo.com"} +{"_id":{"$oid":"503c6720fe4dce143735c7ab"},"membership_status":"ACTIVE","name":"David King","gender":"female","phone_no":"+14304837434","last_login":{"$date":"2013-04-11T21:00:09.000Z"},"address":{"city":"Rochester, New York","street":"33 Westlake St","postal_code":"77991","country":"USA","location":{"type":"Point","coordinates":[-77.6663650168254,43.14447753179987]}},"favorite_feature":"Auth","email":"born-confused54@aim.com"} +{"_id":{"$oid":"503cb09dfe4dce143635c835"},"membership_status":"ACTIVE","name":"Jack N Hill","gender":"female","age":49,"phone_no":"+19430658243","last_login":{"$date":"2012-02-09T08:37:00.000Z"},"address":{"city":"Fort Worth, Texas","street":"380 East Mc Gilvra Pkwy","postal_code":"17225","country":"USA","location":{"type":"Point","coordinates":[-97.31943062253647,32.70516207948639]}},"favorite_feature":"Sharding","email":"dogbone36@hotmail.com"} +{"_id":{"$oid":"503d09bdfe4dce143735c8c7"},"membership_status":"PENDING","name":"Lillian L Allen","gender":"female","phone_no":{"$numberLong":"2263259627"},"last_login":{"$date":"2013-05-09T16:01:52.000Z"},"address":{"city":"Tampa, Florida","street":"48 East St","postal_code":"15360","country":"UK","location":{"type":"Point","coordinates":[-82.46544367437033,27.96402082116518]}},"email":"heavenlybeliever44@verizon.net"} +{"_id":{"$oid":"503d1fd8fe4dce143735cac4"},"membership_status":"ACTIVE","name":"Jack V Clarke","age":33,"phone_no":"+13575306196","last_login":{"$date":"2013-03-06T14:19:58.000Z"},"address":{"city":"Glendale, Arizona","street":"164 S Raymond Rd","postal_code":"54344","country":"USA","location":{"type":"Point","coordinates":[-112.18110072377546,33.50037945391298]}},"favorite_feature":"Text Search","email":"jaycee13@verizon.net"} +{"_id":{"$oid":"503d250afe4dce143535ca17"},"membership_status":"PENDING","name":"Audrey Morgan","gender":"male","phone_no":"+11340017394","last_login":{"$date":"2011-12-23T16:19:19.000Z"},"address":{"city":"Arlington, Virginia","street":"228 Slater Rd","postal_code":37862,"country":"USA","location":{"type":"Point","coordinates":[-77.04282363427767,38.87417702609198]}},"favorite_feature":"Sharding","email":"deathnut91@comcast.net"} +{"_id":{"$oid":"503d29d5fe4dce143535c995"},"membership_status":"PENDING","name":"Olivia S Smith","gender":"female","phone_no":"+17398485292","last_login":{"$date":"2014-04-18T01:45:48.000Z"},"address":{"city":"Newark, New Jersey","street":"233 South Corgiat Rd","postal_code":87499,"country":"USA","location":{"type":"Point","coordinates":[-74.19361383949821,40.706032965635124]}},"email":"theprophet92@gmail.com"} +{"_id":{"$oid":"503d5149fe4dce143635c8d0"},"membership_status":"ACTIVE","name":"Daniel L Cooper","gender":"female","age":47,"phone_no":"+15103711432","last_login":{"$date":"2011-03-05T18:18:41.000Z"},"address":{"city":"Modesto, California","street":"358 Bogny Rd","postal_code":"54889-1361","country":"USA","location":{"type":"Point","coordinates":[-121.00586018229355,37.67783487422151]}},"email":"enchanted96@gmx.net"} +{"_id":{"$oid":"503d5342fe4dce143635c36d"},"membership_status":"ACTIVE","name":"Isabella O Roberts","gender":"female","age":46,"phone_no":"+11043160480","last_login":{"$date":"2011-07-26T01:00:19.000Z"},"address":{"city":"Fort Wayne, Indiana","street":"226 Saint Andrew St","postal_code":16610,"country":"USA","location":{"type":"Point","coordinates":[-85.1563327927226,41.068453483114865]}},"favorite_feature":"Auth","email":"soultaker7@comcast.net"} +{"_id":{"$oid":"503d6d1cfe4dce143635c518"},"membership_status":"PENDING","name":"Ellie Thomas","phone_no":"+15875624428","last_login":{"$date":"2010-01-18T11:05:52.000Z"},"address":{"city":"St. Petersburg, Florida","street":"19 Valley Greene St","postal_code":"61361","country":"USA","location":{"type":"Point","coordinates":[-82.61392915925177,27.72042178110669]}},"favorite_feature":"Partial Indexes","email":"buttercup12@aim.com"} +{"_id":{"$oid":"503d94ccfe4dce143835c859"},"membership_status":"ACTIVE","name":"Evelyn Green","gender":"female","phone_no":1623174675,"last_login":{"$date":"2011-01-14T08:48:10.000Z"},"address":{"city":"Plano, Texas","street":"29 West Plymouth Ave","postal_code":71530,"country":"UK","location":{"type":"Point","coordinates":[-96.75408243594752,32.983625650168875]}},"email":"princess82@gmx.net"} +{"_id":{"$oid":"503db4eafe4dce143535ca5a"},"membership_status":"ACTIVE","name":"Matthew P Brown","phone_no":"+11572864401","last_login":{"$date":"2011-01-14T05:51:17.000Z"},"address":{"city":"Dallas, Texas","street":"115 Daley Rd","postal_code":"98752","country":"USA","location":{"type":"Point","coordinates":[-96.83882843334047,32.834341406278085]}},"favorite_feature":"Document Validation","email":"girlydarkhorse72@icloud.com"} +{"_id":{"$oid":"503db5a9fe4dce143635c1c8"},"membership_status":"ACTIVE","name":"Sofia Davies","gender":"female","age":40,"phone_no":"+499224943326","last_login":{"$date":"2014-09-17T09:02:03.000Z"},"address":{"city":"Portland, Oregon","street":"383 Northeast Roberts Pkwy","postal_code":"30799","country":"USA","location":{"type":"Point","coordinates":[-122.69747685630946,45.51563791543689]}},"favorite_feature":"Document Validation","email":"battledoom48@hotmail.com"} +{"_id":{"$oid":"503df455fe4dce143635c8d9"},"membership_status":"ACTIVE","name":"Isaac L Edwards","gender":"female","age":37,"phone_no":"+18941414539","last_login":{"$date":"2013-12-10T07:22:19.000Z"},"address":{"city":"Portland, Oregon","street":"12 Dellwood St","postal_code":"77489","country":"USA","location":{"type":"Point","coordinates":[-122.69333925736352,45.46897176755713]}},"favorite_feature":"Partial Indexes","email":"swampmasher72@verizon.net"} +{"_id":{"$oid":"503e4fa0fe4dce143735c377"},"membership_status":"ACTIVE","name":"Brooklyn L Lewis","gender":"female","phone_no":"+19956277557","last_login":{"$date":"2011-10-06T13:08:54.000Z"},"address":{"city":"Arlington, Virginia","street":"87 Triland Ln","postal_code":"66952","country":"Germany","location":{"type":"Point","coordinates":[-77.16704297630906,38.8399002683331]}},"favorite_feature":"Document Validation","email":"dollyhitchhiker18@gmx.net"} +{"_id":{"$oid":"503e539afe4dce143535caef"},"membership_status":"ACTIVE","name":"Nora U Clarke","gender":"male","age":32,"phone_no":"+18545273552","last_login":{"$date":"2012-10-03T20:38:41.000Z"},"address":{"city":"Baltimore, Maryland","street":"393 West Lawton St","postal_code":"53525","country":"USA","location":{"type":"Point","coordinates":[-76.59382285943863,39.275085894191044]}},"favorite_feature":"Auth","email":"battledoom9@yahoo.com"} +{"_id":{"$oid":"503e77c9fe4dce143535c639"},"membership_status":"ACTIVE","name":"Isaac Allen","gender":"female","phone_no":"+15398291195","last_login":{"$date":"2014-08-09T06:25:19.000Z"},"address":{"city":"Birmingham, Alabama","street":"297 South Spokane Ln","postal_code":"78351","country":"UK","location":{"type":"Point","coordinates":[-86.80728835562441,33.57523144252437]}},"favorite_feature":"Document Validation","email":"gozmit95@gmx.net"} +{"_id":{"$oid":"503e82b0fe4dce143535c5df"},"membership_status":"ACTIVE","name":"David J Thomas","gender":"female","phone_no":{"$numberLong":"4134701337"},"last_login":{"$date":"2010-03-30T09:12:51.000Z"},"address":{"city":"Denver, Colorado","street":"120 Northeast Maple St","postal_code":"38462","country":"UK","location":{"type":"Point","coordinates":[-104.97010545365144,39.74992324364473]}},"email":"dogbone54@verizon.net"} +{"_id":{"$oid":"503e930bfe4dce143835c8f8"},"membership_status":"ACTIVE","name":"Gabriel U Lee","gender":"female","age":39,"phone_no":"+16297373635","last_login":{"$date":"2014-10-05T04:08:16.000Z"},"address":{"city":"Columbus, Ohio","street":"266 East Spring Ave","postal_code":16643,"country":"UK","location":{"type":"Point","coordinates":[-82.95153341097446,39.95414671849552]}},"email":"cozybutton100@aim.com"} diff --git a/packages/mongodb-schema/examples/parse-from-file.ts b/packages/mongodb-schema/examples/parse-from-file.ts new file mode 100644 index 000000000..c0c235401 --- /dev/null +++ b/packages/mongodb-schema/examples/parse-from-file.ts @@ -0,0 +1,58 @@ +/* eslint no-console: 0 */ + +import { pipeline as callbackPipeline, PassThrough, Transform } from 'stream'; +import path from 'path'; +import fs from 'fs'; +import { promisify } from 'util'; +import { parseSchema } from '../src'; + +const schemaFileName = path.join(__dirname, './fanclub.json'); + +function createFileStreamLineParser() { + let currentLine: undefined | string; + + return new Transform({ + readableObjectMode: true, + transform(chunk, encoding, next) { + const lines = ( + (currentLine !== undefined ? currentLine : '') + String(chunk) + ).split(/\r?\n/); + + currentLine = lines.pop(); + + for (const line of lines) { + this.push(JSON.parse(line)); + } + + next(); + }, + + flush(done) { + if (currentLine) { + this.push(JSON.parse(currentLine)); + } + + done(); + }, + }); +} + +async function parseFromFile(fileName: string) { + const startTime = Date.now(); + + const fileReadStream = fs.createReadStream(fileName, { + flags: 'r', // Open for reading, error if doesn't exist. + }); + + const dest = new PassThrough({ objectMode: true }); + const resultPromise = parseSchema(dest); + const pipeline = promisify(callbackPipeline); + await pipeline(fileReadStream, createFileStreamLineParser(), dest); + const res = await resultPromise; + + const dur = Date.now() - startTime; + console.log(res); + console.log(`Schema analysis took ${dur}ms.`); // Log time it took to parse. +} + +void parseFromFile(schemaFileName); diff --git a/packages/mongodb-schema/examples/parse-schema.ts b/packages/mongodb-schema/examples/parse-schema.ts new file mode 100644 index 000000000..84fd4c1cc --- /dev/null +++ b/packages/mongodb-schema/examples/parse-schema.ts @@ -0,0 +1,27 @@ +/* eslint no-console: 0 */ + +import { MongoClient } from 'mongodb'; + +import parseSchema from '../src'; + +const dbName = 'test'; + +const client = new MongoClient(`mongodb://localhost:27017/${dbName}`); + +async function run() { + const db = client.db(dbName); + + try { + const docs = db.collection('test-schema').find().limit(100); + + const parsedSchema = await parseSchema(docs, {}); + + console.log(JSON.stringify(parsedSchema, null, 2)); + } catch (err) { + return console.error(err); + } finally { + await client.close(); + } +} + +void run(); diff --git a/packages/mongodb-schema/package.json b/packages/mongodb-schema/package.json new file mode 100644 index 000000000..b8c5501e5 --- /dev/null +++ b/packages/mongodb-schema/package.json @@ -0,0 +1,110 @@ +{ + "name": "@mongodb-js/mongodb-schema", + "description": "Infer a probabilistic schema for a MongoDB collection.", + "author": { + "name": "MongoDB Inc", + "email": "compass@mongodb.com" + }, + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://jira.mongodb.org/projects/COMPASS/issues", + "email": "compass@mongodb.com" + }, + "homepage": "https://github.com/mongodb-js/devtools-shared", + "version": "0.1.0", + "repository": { + "type": "git", + "url": "https://github.com/mongodb-js/devtools-shared.git" + }, + "keywords": [ + "mongodb", + "schema" + ], + "bin": { + "mongodb-schema": "bin/mongodb-schema" + }, + "files": [ + "LICENSE", + "dist", + "bin", + "package.json", + "README.md", + ".esm-wrapper.mjs" + ], + "license": "Apache-2.0", + "main": "dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "require": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "import": { + "default": "./.esm-wrapper.mjs", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "bootstrap": "npm run compile", + "prepublishOnly": "npm run compile", + "compile": "tsc -p tsconfig.json && gen-esm-wrapper . ./.esm-wrapper.mjs", + "typecheck": "tsc --noEmit", + "eslint": "eslint", + "prettier": "prettier", + "lint": "npm run eslint . && npm run prettier -- --check .", + "depcheck": "depcheck", + "check": "npm run typecheck && npm run lint && npm run depcheck", + "check-ci": "npm run check", + "test": "npm run compile && npm run test-only", + "test-only": "mocha", + "test-cov": "nyc -x \"**/*.spec.*\" -x \"**/*.test.*\" --reporter=lcov --reporter=text --reporter=html npm run test", + "test-watch": "npm run test-only -- --watch", + "test-ci": "npm run test-cov", + "test-example-parse-from-file": "ts-node examples/parse-from-file.ts", + "test-example-parse-schema": "ts-node examples/parse-schema.ts", + "test-time": "ts-node ./scripts/time-testing.ts", + "reformat": "npm run prettier -- --write ." + }, + "dependencies": { + "reservoir": "^0.1.2" + }, + "optionalDependencies": { + "bson": "^4.6.3 || ^5 || ^6.10.3 || ^7.0.0", + "cli-table": "^0.3.4", + "js-yaml": "^5.2.2", + "mongodb": "^6.9.0 || ^7.0.0", + "mongodb-ns": "^3.2.1", + "numeral": "^2.0.6", + "progress": "^2.0.3", + "stats-lite": "^2.0.0", + "yargs": "^18.0.0" + }, + "devDependencies": { + "@mongodb-js/compass-test-server": "^0.3.2", + "@mongodb-js/eslint-config-devtools": "^0.11.8", + "@mongodb-js/mocha-config-devtools": "^1.1.3", + "@mongodb-js/prettier-config-devtools": "^1.0.4", + "@mongodb-js/tsconfig-devtools": "^1.1.3", + "@types/chai": "^4.2.21", + "@types/json-schema": "^7.0.15", + "@types/mocha": "^9.1.1", + "@types/node": "^25.5.2", + "@types/reservoir": "^0.1.0", + "@types/sinon-chai": "^4.0.0", + "ajv": "^8.18.0", + "bson": "^4.6.3 || ^5 || ^6.10.3 || ^7.0.0", + "chai": "^4.5.0", + "depcheck": "^1.4.7", + "eslint": "^7.25.0 || ^8.0.0", + "gen-esm-wrapper": "^1.1.3", + "mocha": "^8.4.0", + "mongodb": "^7.5.0", + "nyc": "^15.1.0", + "prettier": "^3.8.1", + "sinon": "^9.2.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/packages/mongodb-schema/scripts/time-testing.ts b/packages/mongodb-schema/scripts/time-testing.ts new file mode 100644 index 000000000..26237af5b --- /dev/null +++ b/packages/mongodb-schema/scripts/time-testing.ts @@ -0,0 +1,45 @@ +/* eslint no-console: 0 */ + +import { MongoClient } from 'mongodb'; + +import parseSchema from '../src'; + +const dbName = 'test'; +const collectionName = 'test-schema'; +// const collectionName = 'complex'; +// const collectionName = 'listings'; +// const collectionName = 'all-bson-types'; + +const namespace = `${dbName}.${collectionName}`; + +const client = new MongoClient(`mongodb://localhost:27017/${dbName}`); + +async function run() { + const db = client.db(dbName); + + try { + const docsLimit = 10000; + const docs = await db + .collection(collectionName) + .find() + .limit(docsLimit) + .toArray(); + + console.log( + `Analyzing the schema of ${docs.length} documents from the '${namespace}' namespace...`, + ); + + const startTime = Date.now(); + + await parseSchema(docs, {}); + + const elapsed = Date.now() - startTime; + console.log(`Done. Time elapsed: ${elapsed} ms.`); + } catch (err) { + return console.error(err); + } finally { + await client.close(); + } +} + +void run(); diff --git a/packages/mongodb-schema/src/index.ts b/packages/mongodb-schema/src/index.ts new file mode 100644 index 000000000..7d95f4289 --- /dev/null +++ b/packages/mongodb-schema/src/index.ts @@ -0,0 +1,108 @@ +import type { SchemaAccessor } from './schema-accessor'; +import { InternalSchemaBasedAccessor } from './schema-accessor'; +import { getCompletedSchemaAnalyzer, SchemaAnalyzer } from './schema-analyzer'; +import type { + ArraySchemaType, + BaseSchemaType, + ConstantSchemaType, + DocumentSchemaType, + PrimitiveSchemaType, + SchemaType, + Schema as InternalSchema, + SchemaField, + SchemaParseOptions, + SimplifiedSchemaBaseType, + SimplifiedSchemaArrayType, + SimplifiedSchemaDocumentType, + SimplifiedSchemaType, + SimplifiedSchemaField, + SimplifiedSchema, +} from './schema-analyzer'; +import { convertInternalToExpanded } from './schema-converters/internal-to-expanded'; +import { convertInternalToMongodb } from './schema-converters/internal-to-mongodb'; +import { convertInternalToStandard } from './schema-converters/internal-to-standard'; +import * as schemaStats from './stats'; +import type { + AnyIterable, + StandardJSONSchema, + MongoDBJSONSchema, + ExpandedJSONSchema, + JSONSchema, +} from './types'; +import { toTypescriptTypeDefinition } from './to-typescript'; + +/** + * Analyze documents - schema can be retrieved in different formats. + */ +async function analyzeDocuments( + source: AnyIterable, + options?: SchemaParseOptions, +): Promise { + const internalSchema = ( + await getCompletedSchemaAnalyzer(source, options) + ).getResult(); + return new InternalSchemaBasedAccessor(internalSchema, { + internalToStandard: convertInternalToStandard, + internalToMongoDB: convertInternalToMongodb, + internalToExpanded: convertInternalToExpanded, + }); +} + +/** + * Convenience shortcut for parsing schemas. Accepts an array, stream or + * MongoDB cursor object to parse documents` from. + */ +async function parseSchema( + source: AnyIterable, + options?: Partial, +): Promise { + return (await getCompletedSchemaAnalyzer(source, options)).getResult(); +} + +// Convenience shortcut for getting schema paths. +async function getSchemaPaths(source: AnyIterable): Promise { + return (await getCompletedSchemaAnalyzer(source)).getSchemaPaths(); +} + +// Convenience shortcut for getting the simplified schema. +async function getSimplifiedSchema( + source: AnyIterable, +): Promise { + return (await getCompletedSchemaAnalyzer(source)).getSimplifiedSchema(); +} + +export default parseSchema; + +export type { + ArraySchemaType, + BaseSchemaType, + ConstantSchemaType, + DocumentSchemaType, + PrimitiveSchemaType, + SchemaType, + InternalSchema as Schema, + InternalSchema, + SchemaField, + SchemaParseOptions, + SimplifiedSchemaBaseType, + SimplifiedSchemaArrayType, + SimplifiedSchemaDocumentType, + SimplifiedSchemaType, + SimplifiedSchemaField, + SimplifiedSchema, + StandardJSONSchema, + MongoDBJSONSchema, + ExpandedJSONSchema, + JSONSchema, + SchemaAccessor, +}; + +export { + parseSchema, + analyzeDocuments, + getSchemaPaths, + getSimplifiedSchema, + SchemaAnalyzer, + schemaStats, + toTypescriptTypeDefinition, +}; diff --git a/packages/mongodb-schema/src/schema-accessor.ts b/packages/mongodb-schema/src/schema-accessor.ts new file mode 100644 index 000000000..05ec280c7 --- /dev/null +++ b/packages/mongodb-schema/src/schema-accessor.ts @@ -0,0 +1,80 @@ +import type { Schema as InternalSchema } from './schema-analyzer'; +import type { + ExpandedJSONSchema, + MongoDBJSONSchema, + SchemaConverterFn, + StandardJSONSchema, +} from './types'; + +type Options = { + signal?: AbortSignal; +}; + +export interface SchemaAccessor { + getStandardJsonSchema: (options?: Options) => Promise; + getMongoDBJsonSchema: (options?: Options) => Promise; + getExpandedJSONSchema: (options?: Options) => Promise; + getInternalSchema: (options?: Options) => Promise; +} + +/** + * Accessor for different schema formats. + * Internal schema is provided at initialization, + * the others are converted lazily and memoized. + * Conversion can be aborted. + */ + +export type InternalConverters = { + internalToStandard: SchemaConverterFn; + internalToExpanded: SchemaConverterFn; + internalToMongoDB: SchemaConverterFn; +}; +export class InternalSchemaBasedAccessor implements SchemaAccessor { + private internalSchema: InternalSchema; + private standardJSONSchema?: StandardJSONSchema; + private mongodbJSONSchema?: MongoDBJSONSchema; + private expandedJSONSchema?: ExpandedJSONSchema; + private converters: InternalConverters; + + constructor(internalSchema: InternalSchema, converters: InternalConverters) { + this.internalSchema = internalSchema; + this.converters = converters; + } + + getInternalSchema(): Promise { + return Promise.resolve(this.internalSchema); + } + + /** + * Get standard JSON Schema - as per + * https://json-schema.org/draft/2020-12/schema + */ + async getStandardJsonSchema( + options: Options = {}, + ): Promise { + return (this.standardJSONSchema ??= + await this.converters.internalToStandard(this.internalSchema, options)); + } + + /** + * Get MongoDB's $jsonSchema + */ + async getMongoDBJsonSchema( + options: Options = {}, + ): Promise { + return (this.mongodbJSONSchema ??= await this.converters.internalToMongoDB( + this.internalSchema, + options, + )); + } + + /** + * Get expanded JSON Schema - with additional properties + */ + async getExpandedJSONSchema( + options: Options = {}, + ): Promise { + return (this.expandedJSONSchema ??= + await this.converters.internalToExpanded(this.internalSchema, options)); + } +} diff --git a/packages/mongodb-schema/src/schema-analyzer.ts b/packages/mongodb-schema/src/schema-analyzer.ts new file mode 100644 index 000000000..e8b373d20 --- /dev/null +++ b/packages/mongodb-schema/src/schema-analyzer.ts @@ -0,0 +1,775 @@ +import Reservoir from 'reservoir'; +import type { + Document, + ObjectId, + MinKey, + MaxKey, + Long, + Double, + Int32, + Decimal128, + BSONRegExp, + BSONSymbol, + Timestamp, +} from 'bson'; +import { Binary, Code } from 'bson'; + +import semanticTypes from './semantic-types'; +import type { AnyIterable } from './types'; +import { allowAbort, ALLOW_ABORT_INTERVAL_COUNT } from './util'; + +type TypeCastMap = { + Array: unknown[]; + Binary: Binary; + Boolean: boolean; + Code: Code; + CodeWScope: Code; + Date: Date; + Decimal128: Decimal128; + Double: Double; + Int32: Int32; + Int64: Long; + MaxKey: MaxKey; + MinKey: MinKey; + Null: null; + Object: Record; // Note: In the resulting schema we rename `Object` to `Document`. + ObjectId: ObjectId; + BSONRegExp: BSONRegExp; + String: string; + BSONSymbol: BSONSymbol; + Timestamp: Timestamp; + Undefined: undefined; +}; + +type TypeCastTypes = keyof TypeCastMap; +type BSONValue = TypeCastMap[TypeCastTypes]; + +export type BaseSchemaType = { + path: string[]; + name: string; + count: number; + probability: number; + bsonType: string; + + // As `values` is from a sample reservoir this isn't a true check for duplicates/uniqueness. + // We cannot compute `unique` and `hasDuplicates` when `storeValues` is false. + hasDuplicates?: boolean; + unique?: number; +}; + +export type ConstantSchemaType = BaseSchemaType & { + name: 'Null' | 'Undefined'; +}; + +export type PrimitiveSchemaType = BaseSchemaType & { + name: + | 'String' + | 'Number' + | 'Int32' + | 'Boolean' + | 'Decimal128' + | 'Long' + | 'ObjectId' + | 'Date' + | 'RegExp' + | 'Symbol' + | 'MaxKey' + | 'MinKey' + | 'Binary' + | 'Code' + | 'Timestamp' + | 'DBRef'; + values: BSONValue[]; +}; + +export type ArraySchemaType = BaseSchemaType & { + name: 'Array'; + lengths: number[]; + averageLength: number; + totalCount: number; + // eslint-disable-next-line no-use-before-define + types: SchemaType[]; +}; + +export type DocumentSchemaType = BaseSchemaType & { + name: 'Document'; + // eslint-disable-next-line no-use-before-define + fields: SchemaField[]; +}; + +// We include the base schema type to make the `semantic-types` usage +// easier to type. +export type SchemaType = + | BaseSchemaType + | ConstantSchemaType + | PrimitiveSchemaType + | ArraySchemaType + | DocumentSchemaType; + +export type SchemaField = { + name: string; + count: number; + path: string[]; + type: string | string[]; + probability: number; + hasDuplicates: boolean; + types: SchemaType[]; +}; + +export type Schema = { + count: number; + fields: SchemaField[]; +}; + +type SchemaBSONType = Exclude | 'Document'; + +type SchemaAnalysisBaseType = { + name: string; + path: string[]; + bsonType: SchemaBSONType; + count: number; + values?: ReturnType; +}; + +type SchemaAnalysisNullType = SchemaAnalysisBaseType & { + name: 'Null'; +}; + +type SchemaAnalysisPrimitiveType = SchemaAnalysisBaseType & { + name: + | 'String' + | 'Number' + | 'Int32' + | 'Boolean' + | 'Decimal128' + | 'Long' + | 'ObjectId' + | 'Date' + | 'RegExp' + | 'Symbol' + | 'MaxKey' + | 'MinKey' + | 'Binary' + | 'Code' + | 'Timestamp' + | 'DBRef'; +}; + +type SchemaAnalysisArrayType = SchemaAnalysisBaseType & { + name: 'Array'; + lengths: number[]; + // eslint-disable-next-line no-use-before-define + types: SchemaAnalysisFieldTypes; +}; + +type SchemaAnalysisDocumentType = SchemaAnalysisBaseType & { + name: 'Document'; + // eslint-disable-next-line no-use-before-define + fields: SchemaAnalysisFieldsMap; +}; + +// We include the base schema type to make the `semantic-types` usage +// easier to type. +type SchemaAnalysisType = + | SchemaAnalysisBaseType + | SchemaAnalysisNullType + | SchemaAnalysisPrimitiveType + | SchemaAnalysisArrayType + | SchemaAnalysisDocumentType; + +type SchemaAnalysisFieldTypes = { + [fieldName: string]: SchemaAnalysisType; +}; + +type SchemaAnalysisField = { + name: string; + path: string[]; + count: number; + types: SchemaAnalysisFieldTypes; +}; + +// This is used for the current state of the schema analysis. +type SchemaAnalysisFieldsMap = { + [fieldName: string]: SchemaAnalysisField; +}; + +type SchemaAnalysisRoot = { + fields: SchemaAnalysisFieldsMap; + count: number; +}; + +type SemanticTypeFunction = (value: BSONValue, path?: string[]) => boolean; +type SemanticTypeMap = { + [typeName: string]: SemanticTypeFunction | boolean; +}; + +type AllSchemaParseOptions = { + semanticTypes: boolean | SemanticTypeMap; + storeValues: boolean; + signal?: AbortSignal; + storedValuesLengthLimit: number; + /** Complexity limit: + * The analysis will be aborted if the threshold is exceeded. */ + distinctFieldsAbortThreshold?: number; +}; +export type SchemaParseOptions = Partial; + +const defaultSchemaParseOptions: AllSchemaParseOptions = { + semanticTypes: false, + storeValues: true, + storedValuesLengthLimit: 10000, +}; + +/** + * Extracts a Javascript string from a BSON type to compute unique values. + */ +function extractStringValueFromBSON(value: any): string { + if (value?._bsontype) { + if (['Decimal128', 'Long'].includes(value._bsontype)) { + return value.toString(); + } + if (['Double', 'Int32'].includes(value._bsontype)) { + return String(value.value); + } + } + if (typeof value === 'string') { + return value; + } + return String(value); +} + +function fieldComparator( + a: { + name: string; + }, + b: { + name: string; + }, +) { + // Make sure _id is always at top, even in presence of uppercase fields. + const aName = a.name; + const bName = b.name; + if (aName === '_id') { + return -1; + } + if (bName === '_id') { + return 1; + } + // Otherwise sort case-insensitively. + return aName.toLowerCase() < bName.toLowerCase() ? -1 : 1; +} + +/** + * Returns the type of value as a string. BSON type aware. Replaces `Object` + * with `Document` to avoid naming conflicts with javascript Objects. + */ +function getBSONType(value: any): SchemaBSONType { + const bsonType = value?._bsontype + ? value._bsontype + : Object.prototype.toString.call(value).replace(/^\[object (\w+)\]$/, '$1'); + if (bsonType === 'Object') { + // In the resulting schema we rename `Object` to `Document`. + return 'Document'; + } + if (bsonType === 'Code' && (value as Code).scope) { + return 'CodeWScope'; + } + return bsonType; +} + +function isNullType(type: SchemaAnalysisType): type is SchemaAnalysisNullType { + return (type as SchemaAnalysisNullType).name === 'Null'; +} + +function isArrayType( + type: SchemaAnalysisType, +): type is SchemaAnalysisArrayType { + return (type as SchemaAnalysisArrayType).name === 'Array'; +} + +function isDocumentType( + type: SchemaAnalysisType, +): type is SchemaAnalysisDocumentType { + return (type as SchemaAnalysisDocumentType).name === 'Document'; +} + +/** + * Recursively extracts all of the schema field paths as string arrays. + */ +function schemaToPaths( + fields: SchemaAnalysisFieldsMap, + parent: string[] = [], +): string[][] { + const paths: string[][] = []; + const sortedFields = Object.values(fields).sort(fieldComparator); + + for (const field of sortedFields) { + const path = [...parent, field.name]; + paths.push(path); + + // Recurse on doc. + const doc = Object.values(field.types).find( + (f) => f.bsonType === 'Document', + ) as SchemaAnalysisDocumentType | undefined; + + if (doc) { + paths.push(...schemaToPaths(doc.fields, path)); + } + + // Recurse on array. + const array = Object.values(field.types).find( + (f) => f.bsonType === 'Array', + ) as SchemaAnalysisArrayType | undefined; + if (array) { + const arrayDoc = Object.values(array.types).find( + (f) => f.bsonType === 'Document', + ) as SchemaAnalysisDocumentType | undefined; + + if (arrayDoc) { + paths.push(...schemaToPaths(arrayDoc.fields, path)); + } + } + } + + return paths; +} + +export type SimplifiedSchemaBaseType = { + bsonType: SchemaBSONType; +}; +export type SimplifiedSchemaArrayType = SimplifiedSchemaBaseType & { + bsonType: 'Array'; + // eslint-disable-next-line no-use-before-define + types: SimplifiedSchemaType[]; +}; +export type SimplifiedSchemaDocumentType = SimplifiedSchemaBaseType & { + bsonType: 'Document'; + // eslint-disable-next-line no-use-before-define + fields: SimplifiedSchema; +}; +export type SimplifiedSchemaType = + | SimplifiedSchemaBaseType + | SimplifiedSchemaArrayType + | SimplifiedSchemaDocumentType; +export type SimplifiedSchemaField = { + types: SimplifiedSchemaType[]; +}; +export type SimplifiedSchema = { + [fieldName: string]: SimplifiedSchemaField; +}; + +function simplifiedSchema(fields: SchemaAnalysisFieldsMap): SimplifiedSchema { + function finalizeSchemaFieldTypes( + types: SchemaAnalysisFieldTypes, + ): SimplifiedSchemaType[] { + return Object.values(types) + .sort((a: SchemaAnalysisType, b: SchemaAnalysisType) => { + // Sort the types by what occurs most frequent first. + return b.count - a.count; + }) + .map((type: SchemaAnalysisType) => { + return { + bsonType: type.bsonType, // Note: `Object` is replaced with `Document`. + ...(isArrayType(type) + ? { + types: finalizeSchemaFieldTypes(type.types), + } + : {}), + ...(isDocumentType(type) + ? { fields: finalizeDocumentFieldSchema(type.fields) } + : {}), + }; + }); + } + + function finalizeDocumentFieldSchema( + fieldMap: SchemaAnalysisFieldsMap, + ): SimplifiedSchema { + const fieldSchema: SimplifiedSchema = Object.create(null); + Object.values(fieldMap).forEach((field: SchemaAnalysisField) => { + const fieldTypes = finalizeSchemaFieldTypes(field.types); + + fieldSchema[field.name] = { + types: fieldTypes, + }; + }); + return fieldSchema; + } + + return finalizeDocumentFieldSchema(fields); +} + +function cropString(value: string, limit: number) { + if (limit < 1) return ''; + return value.charCodeAt(limit - 1) === value.codePointAt(limit - 1) + ? value.slice(0, limit) + : value.slice(0, limit - 1); +} + +function getCappedValue( + bsonType: SchemaBSONType, + value: BSONValue, + limit: number, +) { + if (bsonType === 'String') { + return cropString(value as string, limit); + } + if (bsonType === 'Binary') { + value = value as Binary; + return value.buffer.length > limit + ? new Binary(value.buffer.slice(0, limit), value.sub_type) + : value; + } + if (bsonType === 'Code') { + value = value as Code; + return value.code.length >= limit + ? new Code(cropString(value.code, limit), value.scope) + : value; + } + return value; +} + +function computeHasDuplicatesForType( + type: SchemaAnalysisType, + unique?: number, +) { + if (isNullType(type)) { + return type.count > 0; + } + + if (!type.values) { + return undefined; + } + + return unique !== type.values.length; +} + +function computeUniqueForType(type: SchemaAnalysisType) { + if (isNullType(type)) { + return type.count === 0 ? 0 : 1; + } + + if (!type.values) { + return undefined; + } + + // As `values` is from a sample reservoir this isn't a true check for uniqueness. + return new Set(type.values.map(extractStringValueFromBSON)).size; +} + +/** + * Final pass through the result to add missing information: + * - Compute `probability`, `unique`, `hasDuplicates` and + * `averageLength` fields. + * - Add `Undefined` pseudo-types. + * - Collapse `type` arrays to single string if length 1. + * - Turn fields and types objects into arrays to conform with original + * schema parser. + */ +function finalizeSchema(schemaAnalysis: SchemaAnalysisRoot): SchemaField[] { + function finalizeArrayFieldProperties(type: SchemaAnalysisArrayType) { + const totalCount = Object.values(type.types) + .map((v) => v.count) + .reduce((p: number, c: number) => p + c, 0); + + const types = finalizeSchemaFieldTypes(type.types, totalCount); + + return { + types, + totalCount, + lengths: type.lengths, + averageLength: totalCount / type.lengths.length, + }; + } + + function finalizeSchemaFieldTypes( + types: SchemaAnalysisFieldTypes, + parentCount: number, + ): SchemaType[] { + return Object.values(types) + .map((type) => { + const unique = computeUniqueForType(type); + + return { + name: type.name, + path: type.path, + count: type.count, + probability: type.count / parentCount, + unique, + hasDuplicates: computeHasDuplicatesForType(type, unique), + values: isNullType(type) ? undefined : type.values, + bsonType: type.bsonType, // Note: `Object` is replaced with `Document`. + ...(isArrayType(type) ? finalizeArrayFieldProperties(type) : {}), + ...(isDocumentType(type) + ? { fields: finalizeDocumentFieldSchema(type.fields, type.count) } + : {}), + }; + }) + .sort((a, b) => b.probability - a.probability); + } + + function finalizeDocumentFieldSchema( + fieldMap: SchemaAnalysisFieldsMap, + parentCount: number, + ): SchemaField[] { + return Object.values(fieldMap) + .map((field: SchemaAnalysisField): SchemaField => { + const fieldTypes = finalizeSchemaFieldTypes(field.types, parentCount); + + const undefinedCount = parentCount - field.count; + if (undefinedCount > 0) { + // Add the `Undefined` pseudo-type. + fieldTypes.push({ + name: 'Undefined', + bsonType: 'Undefined', + unique: undefinedCount > 1 ? 0 : 1, + hasDuplicates: undefinedCount > 1, + path: field.path, + count: undefinedCount, + probability: undefinedCount / parentCount, + }); + } + + return { + name: field.name, + path: field.path, + count: field.count, + type: + fieldTypes.length === 1 + ? fieldTypes[0].name + : fieldTypes.map((v: SchemaType) => v.name), // Or one value or array. + probability: field.count / parentCount, + hasDuplicates: !!fieldTypes.find((v: SchemaType) => v.hasDuplicates), + types: fieldTypes, + }; + }) + .sort(fieldComparator); + } + + return finalizeDocumentFieldSchema( + schemaAnalysis.fields, + schemaAnalysis.count, + ); +} + +export class SchemaAnalyzer { + semanticTypes: SemanticTypeMap; + options: AllSchemaParseOptions; + documentsAnalyzed = 0; + fieldsCount = 0; + schemaAnalysisRoot: SchemaAnalysisRoot = { + fields: Object.create(null), + count: 0, + }; + + finalized = true; + schemaResult: Schema = { + count: 0, + fields: [], + }; + + // Increments when every field or type is analyzed. + // Useful for occasionally checking if the analysis should be aborted. + fieldAndTypeAnalysisCounter = 0; + + constructor(options?: SchemaParseOptions) { + // Set default options. + this.options = { ...defaultSchemaParseOptions, ...options }; + + this.semanticTypes = { + ...semanticTypes, + }; + + if (typeof this.options.semanticTypes === 'object') { + // Enable existing semantic types that evaluate to true. + const enabledTypes = Object.entries(this.options.semanticTypes) + .filter(([, v]) => typeof v === 'boolean' && v) + .map(([k]) => k.toLowerCase()); + + this.semanticTypes = { + ...Object.entries(this.semanticTypes) + .filter(([k]) => enabledTypes.includes(k.toLowerCase())) + .reduce((p, [k, v]) => ({ ...p, [k]: v }), Object.create(null)), + }; + + Object.entries(this.options.semanticTypes) + .filter(([, v]) => typeof v === 'function') + .forEach(([k, v]) => { + this.semanticTypes[k] = v; + }); + } + } + + allowAbortDuringAnalysis(): Promise { + // Allow aborting the analysis. + if (this.fieldAndTypeAnalysisCounter++ % ALLOW_ABORT_INTERVAL_COUNT === 0) { + void allowAbort(); + } + return Promise.resolve(); + } + + increaseFieldCount() { + if (!this.options.distinctFieldsAbortThreshold) return; + this.fieldsCount++; + if (this.fieldsCount > this.options.distinctFieldsAbortThreshold) { + throw new Error( + `Schema analysis aborted: Fields count above ${this.options.distinctFieldsAbortThreshold}`, + ); + } + } + + getSemanticType(value: BSONValue, path: string[]) { + // Pass value to semantic type detectors, return first match or undefined. + const returnValue = Object.entries(this.semanticTypes) + .filter(([, v]) => { + return (v as SemanticTypeFunction)(value, path); + }) + .map(([k]) => k)[0]; + + return returnValue; + } + + async analyzeDoc(doc: Document) { + this.finalized = false; + /** + * Takes a field value, determines the correct type, handles recursion into + * nested arrays and documents, and passes the value down to `addToValue`. + * Note: This mutates the `schema` argument. + */ + const addToType = async ( + path: string[], + value: BSONValue, + schema: SchemaAnalysisFieldTypes, + ) => { + await this.allowAbortDuringAnalysis(); + const bsonType = getBSONType(value); + // If semantic type detection is enabled, the type is the semantic type + // or the original bson type if no semantic type was detected. If disabled, + // it is always the bson type. + const typeName = this.options.semanticTypes + ? this.getSemanticType(value, path) || bsonType + : bsonType; + if (!schema[typeName]) { + schema[typeName] = { + name: typeName, + bsonType: bsonType, + path, + count: 0, + }; + } + const type = schema[typeName]; + type.count++; + + if (isArrayType(type)) { + // Recurse into arrays by calling `addToType` for each element. + type.types = type.types ?? Object.create(null); + type.lengths = type.lengths ?? []; + type.lengths.push((value as BSONValue[]).length); + for (const v of value as BSONValue[]) { + await addToType(path, v, type.types); + } + } else if (isDocumentType(type)) { + // Recurse into nested documents by calling `addToField` for all sub-fields. + type.fields = type.fields ?? Object.create(null); + + for (const [fieldName, v] of Object.entries(value as Document)) { + await addToField(fieldName, [...path, fieldName], v, type.fields); + } + } else if (this.options.storeValues && !isNullType(type)) { + // When the `storeValues` option is enabled, store some example values. + if (!type.values) { + type.values = ['String', 'Binary', 'Code'].includes(bsonType) + ? Reservoir(100) + : Reservoir(10000); + } + + type.values.pushSome( + getCappedValue( + type.bsonType, + value, + this.options.storedValuesLengthLimit, + ), + ); + } + }; + + /** + * Handles a field from a document. Passes the value to `addToType`. + * Note: This mutates the `schema` argument. + */ + const addToField = async ( + fieldName: string, + path: string[], + value: BSONValue, + schema: SchemaAnalysisFieldsMap, + ) => { + await this.allowAbortDuringAnalysis(); + if (!schema[fieldName]) { + schema[fieldName] = { + name: fieldName, + path: path, + count: 0, + types: Object.create(null), + }; + this.increaseFieldCount(); + } + const field = schema[fieldName]; + + field.count++; + await addToType(path, value, field.types); + }; + + for (const key of Object.keys(doc)) { + await addToField(key, [key], doc[key], this.schemaAnalysisRoot.fields); + } + this.schemaAnalysisRoot.count += 1; + } + + getResult(): Schema { + if (this.finalized) { + return this.schemaResult; + } + + this.schemaResult = { + count: this.schemaAnalysisRoot.count, + fields: finalizeSchema(this.schemaAnalysisRoot), + }; + + this.finalized = true; + return this.schemaResult; + } + + getSchemaPaths(): string[][] { + return schemaToPaths(this.schemaAnalysisRoot.fields); + } + + /** + * Returns a simplified schema result, this has no type metadata. + */ + getSimplifiedSchema(): SimplifiedSchema { + return simplifiedSchema(this.schemaAnalysisRoot.fields); + } +} + +export function verifyStreamSource(source: AnyIterable): AnyIterable { + if (!(Symbol.iterator in source) && !(Symbol.asyncIterator in source)) { + throw new Error( + 'Unknown input type for `docs`. Must be an array, ' + + 'stream or MongoDB Cursor.', + ); + } + + return source; +} + +export async function getCompletedSchemaAnalyzer( + source: AnyIterable, + options?: SchemaParseOptions, +): Promise { + const analyzer = new SchemaAnalyzer(options); + for await (const doc of verifyStreamSource(source)) { + if (options?.signal?.aborted) throw options.signal.reason; + await analyzer.analyzeDoc(doc); + } + return analyzer; +} diff --git a/packages/mongodb-schema/src/schema-converters/internal-to-expanded.test.ts b/packages/mongodb-schema/src/schema-converters/internal-to-expanded.test.ts new file mode 100644 index 000000000..a00354114 --- /dev/null +++ b/packages/mongodb-schema/src/schema-converters/internal-to-expanded.test.ts @@ -0,0 +1,1283 @@ +import assert from 'assert'; +import { RELAXED_EJSON_DEFINITIONS } from './internal-to-standard'; +import { convertInternalToExpanded } from './internal-to-expanded'; + +describe('internalSchemaToExpanded', function () { + describe('Converts: ', function () { + it('various types', async function () { + const internal = { + count: 1, + fields: [ + // types with ref + { + name: '_id', + path: ['_id'], + count: 1, + type: 'ObjectId', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'ObjectId', + path: ['_id'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['642d766b7300158b1f22e972'], + bsonType: 'ObjectId', + }, + ], + }, + { + name: 'binaries', + path: ['binaries'], + count: 1, + type: 'Document', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: ['binaries'], + count: 1, + probability: 0.8, + bsonType: 'Document', + fields: [ + { + name: 'binaryOld', + path: ['binaries', 'binaryOld'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'binaryOld'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['//8='], + bsonType: 'Binary', + }, + ], + }, + ], + }, + ], + }, + { + name: 'binData', + path: ['binData'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binData'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['AQID'], + bsonType: 'Binary', + }, + ], + }, + // type with different standard and bsonType + { + name: 'boolean', + path: ['boolean'], + count: 1, + type: 'Boolean', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Boolean', + path: ['boolean'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [true], + bsonType: 'Boolean', + }, + ], + }, + { + name: 'decimal', + path: ['decimal'], + count: 1, + type: 'Decimal128', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Decimal128', + path: ['decimal'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $numberDecimal: '5.477284286264328586719275128128001E-4088', + }, + ], + bsonType: 'Decimal128', + }, + ], + }, + { + name: 'double', + path: ['double'], + count: 1, + type: 'Double', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Double', + path: ['double'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [1.2], + bsonType: 'Double', + }, + ], + }, + { + name: 'javascript', + path: ['javascript'], + count: 1, + type: 'Code', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Code', + path: ['javascript'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + code: 'function() {}', + }, + ], + bsonType: 'Code', + }, + ], + }, + { + name: 'long', + path: ['long'], + count: 1, + type: 'Long', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Long', + path: ['long'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + low: -1395630315, + high: 28744523, + unsigned: false, + }, + ], + bsonType: 'Long', + }, + ], + }, + { + name: 'maxKey', + path: ['maxKey'], + count: 1, + type: 'MaxKey', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'MaxKey', + path: ['maxKey'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [{}], + bsonType: 'MaxKey', + }, + ], + }, + { + name: 'regex', + path: ['regex'], + count: 1, + type: 'BSONRegExp', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'BSONRegExp', + path: ['regex'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + pattern: 'pattern', + options: 'i', + }, + ], + bsonType: 'BSONRegExp', + }, + ], + }, + { + name: 'timestamp', + path: ['timestamp'], + count: 1, + type: 'Timestamp', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Timestamp', + path: ['timestamp'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $timestamp: '7218556297505931265', + }, + ], + bsonType: 'Timestamp', + }, + ], + }, + ], + }; + const expanded = await convertInternalToExpanded(internal); + const expectedDefinitions: Partial = { + ...RELAXED_EJSON_DEFINITIONS, + }; + delete expectedDefinitions.BSONSymbol; + delete expectedDefinitions.CodeWScope; + delete expectedDefinitions.DBPointer; + delete expectedDefinitions.DBRef; + delete expectedDefinitions.Date; + delete expectedDefinitions.MinKey; + delete expectedDefinitions.Undefined; + assert.deepStrictEqual(expanded, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: expectedDefinitions, + properties: { + _id: { + $ref: '#/$defs/ObjectId', + 'x-bsonType': 'objectId', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': ['642d766b7300158b1f22e972'], + }, + binData: { + $ref: '#/$defs/Binary', + 'x-bsonType': 'binData', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': ['AQID'], + }, + binaries: { + type: 'object', + 'x-bsonType': 'object', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + properties: { + binaryOld: { + $ref: '#/$defs/Binary', + 'x-bsonType': 'binData', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': ['//8='], + }, + }, + required: [], + }, + boolean: { + type: 'boolean', + 'x-bsonType': 'bool', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': [true], + }, + decimal: { + $ref: '#/$defs/Decimal128', + 'x-bsonType': 'decimal', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': [ + { + $numberDecimal: '5.477284286264328586719275128128001E-4088', + }, + ], + }, + double: { + $ref: '#/$defs/Double', + 'x-bsonType': 'double', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': [1.2], + }, + javascript: { + $ref: '#/$defs/Code', + 'x-bsonType': 'javascript', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': [ + { + code: 'function() {}', + }, + ], + }, + long: { + type: 'integer', + 'x-bsonType': 'long', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': [ + { + low: -1395630315, + high: 28744523, + unsigned: false, + }, + ], + }, + maxKey: { + $ref: '#/$defs/MaxKey', + 'x-bsonType': 'maxKey', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': [{}], + }, + regex: { + $ref: '#/$defs/RegExp', + 'x-bsonType': 'regex', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': [ + { + options: 'i', + pattern: 'pattern', + }, + ], + }, + timestamp: { + $ref: '#/$defs/Timestamp', + 'x-bsonType': 'timestamp', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': [ + { + $timestamp: '7218556297505931265', + }, + ], + }, + }, + }); + }); + + it('nested document/object', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'author', + path: ['author'], + count: 1, + type: ['Document', 'Undefined'], + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: ['author'], + count: 1, + probability: 0.5, + bsonType: 'Document', + fields: [ + { + name: 'name', + path: ['author', 'name'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['author', 'name'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['Peter Sonder'], + bsonType: 'String', + }, + ], + }, + { + name: 'rating', + path: ['author', 'rating'], + count: 1, + type: 'Double', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Double', + path: ['author', 'rating'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: [1.3], + bsonType: 'Double', + }, + ], + }, + ], + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['author'], + count: 1, + probability: 0.5, + }, + ], + }, + ], + }; + const expanded = await convertInternalToExpanded(internal); + const expectedDefinitions = { + Double: RELAXED_EJSON_DEFINITIONS.Double, + }; + assert.deepStrictEqual(expanded, { + type: 'object', + 'x-bsonType': 'object', + required: ['author'], + $defs: expectedDefinitions, + properties: { + author: { + type: 'object', + 'x-bsonType': 'object', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 1, + }, + required: ['name', 'rating'], + properties: { + name: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 1, + }, + 'x-sampleValues': ['Peter Sonder'], + }, + rating: { + $ref: '#/$defs/Double', + 'x-bsonType': 'double', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 1, + }, + 'x-sampleValues': [1.3], + }, + }, + }, + }, + }); + }); + + describe('arrays', function () { + it('array - single type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'genres', + path: ['genres'], + count: 1, + type: ['array', 'Undefined'], + probability: 0.5, + hasDuplicates: false, + types: [ + { + name: 'array', + path: ['genres'], + count: 1, + probability: 0.5, + bsonType: 'Array', + types: [ + { + name: 'String', + path: ['genres'], + count: 2, + probability: 1, + unique: 2, + hasDuplicates: false, + values: ['crimi', 'comedy'], + bsonType: 'String', + }, + ], + totalCount: 2, + lengths: [2], + averageLength: 2, + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['genres'], + count: 1, + probability: 0.5, + }, + ], + }, + ], + }; + const expanded = await convertInternalToExpanded(internal); + assert.deepStrictEqual(expanded, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: {}, + properties: { + genres: { + type: 'array', + 'x-bsonType': 'array', + 'x-metadata': { + probability: 0.5, + hasDuplicates: false, + count: 1, + }, + items: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 2, + probability: 1, + hasDuplicates: false, + }, + 'x-sampleValues': ['crimi', 'comedy'], + }, + }, + }, + }); + }); + + it('array - mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'genres', + path: ['genres'], + count: 1, + type: ['Array', 'Undefined'], + probability: 0.5, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['genres'], + count: 1, + probability: 0.5, + bsonType: 'Array', + types: [ + { + name: 'String', + path: ['genres'], + count: 2, + probability: 0.6666666666666666, + unique: 2, + hasDuplicates: false, + values: ['crimi', 'comedy'], + bsonType: 'String', + }, + { + name: 'Document', + path: ['genres'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'long', + path: ['genres', 'long'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['genres', 'long'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['science fiction'], + bsonType: 'String', + }, + ], + }, + { + name: 'short', + path: ['genres', 'short'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['genres', 'short'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['scifi'], + bsonType: 'String', + }, + ], + }, + ], + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['genres'], + count: 1, + probability: 0.5, + }, + ], + }, + ], + }; + const expanded = await convertInternalToExpanded(internal); + assert.deepStrictEqual(expanded, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: {}, + properties: { + genres: { + type: 'array', + 'x-bsonType': 'array', + 'x-metadata': { + probability: 0.5, + hasDuplicates: false, + count: 1, + }, + items: { + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 2, + probability: 0.6666666666666666, + hasDuplicates: false, + }, + 'x-sampleValues': ['crimi', 'comedy'], + }, + { + type: 'object', + 'x-bsonType': 'object', + 'x-metadata': { + count: 1, + probability: 0.3333333333333333, + }, + required: ['long', 'short'], + properties: { + long: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + probability: 1, + hasDuplicates: false, + }, + 'x-sampleValues': ['science fiction'], + }, + short: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + probability: 1, + hasDuplicates: false, + }, + 'x-sampleValues': ['scifi'], + }, + }, + }, + ], + }, + }, + }, + }); + }); + + it('array - simple mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'arrayMixedType', + path: ['arrayMixedType'], + count: 1, + type: 'Array', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['arrayMixedType'], + count: 1, + probability: 1, + bsonType: 'Array', + types: [ + { + name: 'int32', + path: ['arrayMixedType'], + count: 2, + probability: 0.6666666666666666, + unique: 2, + hasDuplicates: false, + values: [1, 3], + bsonType: 'Int32', + }, + { + name: 'String', + path: ['arrayMixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: ['2'], + bsonType: 'String', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + ], + }, + ], + }; + const expanded = await convertInternalToExpanded(internal); + assert.deepStrictEqual(expanded, { + type: 'object', + 'x-bsonType': 'object', + required: ['arrayMixedType'], + $defs: {}, + properties: { + arrayMixedType: { + type: 'array', + 'x-bsonType': 'array', + 'x-metadata': { + count: 1, + probability: 1, + hasDuplicates: false, + }, + items: { + anyOf: [ + { + type: 'integer', + 'x-bsonType': 'int', + 'x-metadata': { + count: 2, + hasDuplicates: false, + probability: 0.6666666666666666, + }, + 'x-sampleValues': [1, 3], + }, + { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.3333333333333333, + }, + 'x-sampleValues': ['2'], + }, + ], + }, + }, + }, + }); + }); + }); + + describe('mixed types', function () { + it('simple mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedType', + path: ['mixedType'], + count: 2, + type: ['Int32', 'String', 'Undefined'], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Int32', + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: [1], + bsonType: 'Int32', + }, + { + name: 'String', + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: ['abc'], + bsonType: 'String', + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + }, + ], + }, + ], + }; + const expanded = await convertInternalToExpanded(internal); + assert.deepStrictEqual(expanded, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: {}, + properties: { + mixedType: { + 'x-metadata': { + probability: 0.6666666666666666, + hasDuplicates: false, + count: 2, + }, + anyOf: [ + { + type: 'integer', + 'x-bsonType': 'int', + 'x-metadata': { + probability: 0.3333333333333333, + hasDuplicates: false, + count: 1, + }, + 'x-sampleValues': [1], + }, + { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + probability: 0.3333333333333333, + hasDuplicates: false, + count: 1, + }, + 'x-sampleValues': ['abc'], + }, + ], + }, + }, + }); + }); + + it('complex mixed type (with array and object)', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedComplexType', + path: ['mixedComplexType'], + count: 2, + type: ['Array', 'Document', 'Undefined'], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Array', + types: [ + { + name: 'Int32', + path: ['mixedComplexType'], + count: 3, + probability: 1, + unique: 3, + hasDuplicates: false, + values: [1, 2, 3], + bsonType: 'Int32', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + { + name: 'Document', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'a', + path: ['mixedComplexType', 'a'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['mixedComplexType', 'a'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['bc'], + bsonType: 'String', + }, + ], + }, + ], + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + }, + ], + }, + ], + }; + const expanded = await convertInternalToExpanded(internal); + assert.deepStrictEqual(expanded, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: {}, + properties: { + mixedComplexType: { + 'x-metadata': { + probability: 0.6666666666666666, + hasDuplicates: false, + count: 2, + }, + anyOf: [ + { + type: 'array', + 'x-bsonType': 'array', + 'x-metadata': { + count: 1, + probability: 0.3333333333333333, + }, + items: { + type: 'integer', + 'x-bsonType': 'int', + 'x-metadata': { + probability: 1, + hasDuplicates: false, + count: 3, + }, + 'x-sampleValues': [1, 2, 3], + }, + }, + { + type: 'object', + 'x-bsonType': 'object', + required: ['a'], + 'x-metadata': { + count: 1, + probability: 0.3333333333333333, + }, + properties: { + a: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + probability: 1, + hasDuplicates: false, + count: 1, + }, + 'x-sampleValues': ['bc'], + }, + }, + }, + ], + }, + }, + }); + }); + + it('complex mixed type (with $refs)', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedType', + path: ['mixedType'], + count: 2, + type: ['String', 'ObjectId'], + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: ['abc'], + bsonType: 'String', + }, + { + name: 'ObjectId', + path: ['objectId'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['642d766c7300158b1f22e975'], + bsonType: 'ObjectId', + }, + ], + }, + ], + }; + const expanded = await convertInternalToExpanded(internal); + const expectedDefinitions = { + ObjectId: RELAXED_EJSON_DEFINITIONS.ObjectId, + }; + assert.deepStrictEqual(expanded, { + type: 'object', + 'x-bsonType': 'object', + required: ['mixedType'], + $defs: expectedDefinitions, + properties: { + mixedType: { + 'x-metadata': { + count: 2, + hasDuplicates: false, + probability: 1, + }, + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.3333333333333333, + }, + 'x-sampleValues': ['abc'], + }, + { + $ref: '#/$defs/ObjectId', + 'x-bsonType': 'objectId', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8, + }, + 'x-sampleValues': ['642d766c7300158b1f22e975'], + }, + ], + }, + }, + }); + }); + }); + + it('can be aborted', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedComplexType', + path: ['mixedComplexType'], + count: 2, + type: ['Array', 'Document', 'Undefined'], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Array', + types: [ + { + name: 'Int32', + path: ['mixedComplexType'], + count: 3, + probability: 1, + unique: 3, + hasDuplicates: false, + values: [1, 2, 3], + bsonType: 'Int32', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + { + name: 'Document', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'a', + path: ['mixedComplexType', 'a'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['mixedComplexType', 'a'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['bc'], + bsonType: 'String', + }, + ], + }, + ], + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + }, + ], + }, + ], + }; + const abortController = new AbortController(); + const promise = convertInternalToExpanded(internal, { + signal: abortController.signal, + }); + abortController.abort(new Error("Too long, didn't wait.")); + await assert.rejects(promise, { + name: 'Error', + message: "Too long, didn't wait.", + }); + }); + }); +}); diff --git a/packages/mongodb-schema/src/schema-converters/internal-to-expanded.ts b/packages/mongodb-schema/src/schema-converters/internal-to-expanded.ts new file mode 100644 index 000000000..769342347 --- /dev/null +++ b/packages/mongodb-schema/src/schema-converters/internal-to-expanded.ts @@ -0,0 +1,151 @@ +import type { + ArraySchemaType, + DocumentSchemaType, + Schema as InternalSchema, + SchemaType, + SchemaField, +} from '../schema-analyzer'; +import { type ExpandedJSONSchema } from '../types'; +import { + InternalTypeToStandardTypeMap, + RELAXED_EJSON_DEFINITIONS, +} from './internal-to-standard'; +import { InternalTypeToBsonTypeMap } from './internal-to-mongodb'; +import { allowAbort } from '../util'; + +const createConvertInternalToExpanded = function () { + const usedDefinitions = new Set(); + + function getUsedDefinitions() { + const filteredDefinitions = Object.fromEntries( + Object.entries(RELAXED_EJSON_DEFINITIONS).filter(([key]) => + usedDefinitions.has(key), + ), + ); + return Object.freeze(filteredDefinitions); + } + + function markUsedDefinition(ref: string) { + usedDefinitions.add(ref.split('/')[2]); + } + + function getStandardType(internalType: string) { + const type = InternalTypeToStandardTypeMap[internalType]; + if (!type) throw new Error(`Encountered unknown type: ${internalType}`); + return { ...type }; + } + + function getBsonType(internalType: string) { + const type = InternalTypeToBsonTypeMap[internalType]; + if (!type) throw new Error(`Encountered unknown type: ${internalType}`); + return type; + } + + async function parseType( + type: SchemaType, + signal?: AbortSignal, + ): Promise { + await allowAbort(signal); + const schema: ExpandedJSONSchema = { + ...getStandardType(type.bsonType), + 'x-bsonType': getBsonType(type.bsonType), + 'x-metadata': getMetadata(type), + }; + if ('values' in type && type.values) { + schema['x-sampleValues'] = type.values; + } + if (schema.$ref) markUsedDefinition(schema.$ref); + switch (type.bsonType) { + case 'Array': + schema.items = await parseTypes( + (type as ArraySchemaType).types, + signal, + ); + break; + case 'Document': + Object.assign( + schema, + await parseFields((type as DocumentSchemaType).fields, signal), + ); + break; + } + return schema; + } + + function getMetadata({ + hasDuplicates, + probability, + count, + }: TType) { + return { + ...(typeof hasDuplicates === 'boolean' ? { hasDuplicates } : {}), + probability, + count, + }; + } + + async function parseTypes( + types: SchemaType[], + signal?: AbortSignal, + ): Promise { + await allowAbort(signal); + const definedTypes = types.filter( + (type) => type.bsonType.toLowerCase() !== 'undefined', + ); + const isSingleType = definedTypes.length === 1; + if (isSingleType) { + return parseType(definedTypes[0], signal); + } + const parsedTypes = await Promise.all( + definedTypes.map((type) => parseType(type, signal)), + ); + return { + anyOf: parsedTypes, + }; + } + + async function parseFields( + fields: DocumentSchemaType['fields'], + signal?: AbortSignal, + ): Promise<{ + required: ExpandedJSONSchema['required']; + properties: ExpandedJSONSchema['properties']; + }> { + const required = []; + const properties: ExpandedJSONSchema['properties'] = {}; + for (const field of fields) { + if (field.probability === 1) required.push(field.name); + properties[field.name] = { + ...(await parseTypes(field.types, signal)), + 'x-metadata': getMetadata(field), + }; + } + + return { required, properties }; + } + + return async function convert( + internalSchema: InternalSchema, + options: { signal?: AbortSignal } = {}, + ): Promise { + const { required, properties } = await parseFields( + internalSchema.fields, + options.signal, + ); + const schema: ExpandedJSONSchema = { + type: 'object', + 'x-bsonType': 'object', + required, + properties, + $defs: getUsedDefinitions(), + }; + return schema; + }; +}; + +export function convertInternalToExpanded( + internalSchema: InternalSchema, + options: { signal?: AbortSignal } = {}, +): Promise { + return createConvertInternalToExpanded()(internalSchema, options); +} diff --git a/packages/mongodb-schema/src/schema-converters/internal-to-mongodb.test.ts b/packages/mongodb-schema/src/schema-converters/internal-to-mongodb.test.ts new file mode 100644 index 000000000..63ecd9ad7 --- /dev/null +++ b/packages/mongodb-schema/src/schema-converters/internal-to-mongodb.test.ts @@ -0,0 +1,1403 @@ +import assert from 'assert'; +import { convertInternalToMongodb } from './internal-to-mongodb'; + +describe('internalSchemaToMongoDB', function () { + describe('Converts: ', function () { + it('all the types', async function () { + const internal = { + count: 1, + fields: [ + { + name: '_id', + path: ['_id'], + count: 1, + type: 'ObjectId', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'ObjectId', + path: ['_id'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['642d766b7300158b1f22e972'], + bsonType: 'ObjectId', + }, + ], + }, + { + name: 'array', + path: ['array'], + count: 1, + type: 'Array', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['array'], + count: 1, + probability: 0.8, + bsonType: 'Array', + types: [ + { + name: 'Number', + path: ['array'], + count: 3, + probability: 0.8, + unique: 3, + hasDuplicates: false, + values: [1, 2, 3], + bsonType: 'Number', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + ], + }, + { + name: 'binaries', + path: ['binaries'], + count: 1, + type: 'Document', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: ['binaries'], + count: 1, + probability: 0.8, + bsonType: 'Document', + fields: [ + { + name: 'binaryOld', + path: ['binaries', 'binaryOld'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'binaryOld'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['//8='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'compressedTimeSeries', + path: ['binaries', 'compressedTimeSeries'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'compressedTimeSeries'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['c//SZESzTGmQ6OfR38A11A=='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'custom', + path: ['binaries', 'custom'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'custom'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['//8='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'encrypted', + path: ['binaries', 'encrypted'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'encrypted'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['c//SZESzTGmQ6OfR38A11A=='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'functionData', + path: ['binaries', 'functionData'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'functionData'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['//8='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'generic', + path: ['binaries', 'generic'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'generic'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['AQID'], + bsonType: 'Binary', + }, + ], + }, + { + name: 'md5', + path: ['binaries', 'md5'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'md5'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['c//SZESzTGmQ6OfR38A11A=='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'uuid', + path: ['binaries', 'uuid'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'uuid'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa'], + bsonType: 'Binary', + }, + ], + }, + { + name: 'uuidOld', + path: ['binaries', 'uuidOld'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'uuidOld'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['c//SZESzTGmQ6OfR38A11A=='], + bsonType: 'Binary', + }, + ], + }, + ], + }, + ], + }, + { + name: 'binData', + path: ['binData'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binData'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['AQID'], + bsonType: 'Binary', + }, + ], + }, + { + name: 'boolean', + path: ['boolean'], + count: 1, + type: 'Boolean', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Boolean', + path: ['boolean'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [true], + bsonType: 'Boolean', + }, + ], + }, + { + name: 'date', + path: ['date'], + count: 1, + type: 'Date', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Date', + path: ['date'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['2023-04-05T13:25:08.445Z'], + bsonType: 'Date', + }, + ], + }, + { + name: 'dbRef', + path: ['dbRef'], + count: 1, + type: 'DBRef', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'DBRef', + path: ['dbRef'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $ref: 'namespace', + $id: '642d76b4b7ebfab15d3c4a78', + }, + ], + bsonType: 'DBRef', + }, + ], + }, + { + name: 'decimal', + path: ['decimal'], + count: 1, + type: 'Decimal128', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Decimal128', + path: ['decimal'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $numberDecimal: '5.477284286264328586719275128128001E-4088', + }, + ], + bsonType: 'Decimal128', + }, + ], + }, + { + name: 'double', + path: ['double'], + count: 1, + type: 'Double', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Double', + path: ['double'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [1.2], + bsonType: 'Double', + }, + ], + }, + { + name: 'int', + path: ['int'], + count: 1, + type: 'Int32', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Int32', + path: ['int'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [12345], + bsonType: 'Int32', + }, + ], + }, + { + name: 'javascript', + path: ['javascript'], + count: 1, + type: 'Code', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Code', + path: ['javascript'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + code: 'function() {}', + }, + ], + bsonType: 'Code', + }, + ], + }, + { + name: 'javascriptWithScope', + path: ['javascriptWithScope'], + count: 1, + type: 'CodeWScope', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'CodeWScope', + path: ['javascriptWithScope'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + code: 'function() {}', + scope: { + foo: 1, + bar: 'a', + }, + }, + ], + bsonType: 'CodeWScope', + }, + ], + }, + { + name: 'long', + path: ['long'], + count: 1, + type: 'Long', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Long', + path: ['long'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + low: -1395630315, + high: 28744523, + unsigned: false, + }, + ], + bsonType: 'Long', + }, + ], + }, + { + name: 'maxKey', + path: ['maxKey'], + count: 1, + type: 'MaxKey', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'MaxKey', + path: ['maxKey'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [{}], + bsonType: 'MaxKey', + }, + ], + }, + { + name: 'minKey', + path: ['minKey'], + count: 1, + type: 'MinKey', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'MinKey', + path: ['minKey'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [{}], + bsonType: 'MinKey', + }, + ], + }, + { + name: 'null', + path: ['null'], + count: 1, + type: 'Null', + probability: 0.8, + hasDuplicates: true, + types: [ + { + name: 'Null', + path: ['null'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: true, + bsonType: 'Null', + }, + ], + }, + { + name: 'object', + path: ['object'], + count: 1, + type: 'Document', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: ['object'], + count: 1, + probability: 0.8, + bsonType: 'Document', + fields: [ + { + name: 'key', + path: ['object', 'key'], + count: 1, + type: 'String', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['object', 'key'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['value'], + bsonType: 'String', + }, + ], + }, + ], + }, + ], + }, + { + name: 'objectId', + path: ['objectId'], + count: 1, + type: 'ObjectId', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'ObjectId', + path: ['objectId'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['642d766c7300158b1f22e975'], + bsonType: 'ObjectId', + }, + ], + }, + { + name: 'regex', + path: ['regex'], + count: 1, + type: 'BSONRegExp', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'BSONRegExp', + path: ['regex'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + pattern: 'pattern', + options: 'i', + }, + ], + bsonType: 'BSONRegExp', + }, + ], + }, + { + name: 'string', + path: ['string'], + count: 1, + type: 'String', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['string'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['Hello, world!'], + bsonType: 'String', + }, + ], + }, + { + name: 'symbol', + path: ['symbol'], + count: 1, + type: 'BSONSymbol', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'BSONSymbol', + path: ['symbol'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['symbol'], + bsonType: 'BSONSymbol', + }, + ], + }, + { + name: 'timestamp', + path: ['timestamp'], + count: 1, + type: 'Timestamp', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Timestamp', + path: ['timestamp'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $timestamp: '7218556297505931265', + }, + ], + bsonType: 'Timestamp', + }, + ], + }, + ], + }; + const mongodb = await convertInternalToMongodb(internal); + assert.deepStrictEqual(mongodb, { + bsonType: 'object', + properties: { + _id: { + bsonType: 'objectId', + }, + array: { + bsonType: 'array', + items: { + bsonType: 'double', + }, + }, + binData: { + bsonType: 'binData', + }, + binaries: { + bsonType: 'object', + properties: { + binaryOld: { + bsonType: 'binData', + }, + compressedTimeSeries: { + bsonType: 'binData', + }, + custom: { + bsonType: 'binData', + }, + encrypted: { + bsonType: 'binData', + }, + functionData: { + bsonType: 'binData', + }, + generic: { + bsonType: 'binData', + }, + md5: { + bsonType: 'binData', + }, + uuid: { + bsonType: 'binData', + }, + uuidOld: { + bsonType: 'binData', + }, + }, + }, + boolean: { + bsonType: 'bool', + }, + date: { + bsonType: 'date', + }, + dbRef: { + bsonType: 'dbPointer', + }, + decimal: { + bsonType: 'decimal', + }, + double: { + bsonType: 'double', + }, + int: { + bsonType: 'int', + }, + javascript: { + bsonType: 'javascript', + }, + javascriptWithScope: { + bsonType: 'javascriptWithScope', + }, + long: { + bsonType: 'long', + }, + maxKey: { + bsonType: 'maxKey', + }, + minKey: { + bsonType: 'minKey', + }, + null: { + bsonType: 'null', + }, + object: { + bsonType: 'object', + properties: { + key: { + bsonType: 'string', + }, + }, + }, + objectId: { + bsonType: 'objectId', + }, + regex: { + bsonType: 'regex', + }, + string: { + bsonType: 'string', + }, + symbol: { + bsonType: 'symbol', + }, + timestamp: { + bsonType: 'timestamp', + }, + }, + }); + }); + + it('nested document/object', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'author', + path: ['author'], + count: 1, + type: ['Document', 'Undefined'], + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: ['author'], + count: 1, + probability: 0.5, + bsonType: 'Document', + fields: [ + { + name: 'name', + path: ['author', 'name'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['author', 'name'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['Peter Sonder'], + bsonType: 'String', + }, + ], + }, + { + name: 'rating', + path: ['author', 'rating'], + count: 1, + type: 'Double', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Double', + path: ['author', 'rating'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: [1.3], + bsonType: 'Double', + }, + ], + }, + ], + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['author'], + count: 1, + probability: 0.5, + }, + ], + }, + ], + }; + const mongodb = await convertInternalToMongodb(internal); + assert.deepStrictEqual(mongodb, { + bsonType: 'object', + required: ['author'], + properties: { + author: { + bsonType: 'object', + required: ['name', 'rating'], + properties: { + name: { + bsonType: 'string', + }, + rating: { + bsonType: 'double', + }, + }, + }, + }, + }); + }); + + describe('arrays', function () { + it('array - single type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'genres', + path: ['genres'], + count: 1, + type: ['array', 'Undefined'], + probability: 0.5, + hasDuplicates: false, + types: [ + { + name: 'array', + path: ['genres'], + count: 1, + probability: 0.5, + bsonType: 'Array', + types: [ + { + name: 'String', + path: ['genres'], + count: 2, + probability: 1, + unique: 2, + hasDuplicates: false, + values: ['crimi', 'comedy'], + bsonType: 'String', + }, + ], + totalCount: 2, + lengths: [2], + averageLength: 2, + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['genres'], + count: 1, + probability: 0.5, + }, + ], + }, + ], + }; + const mongodb = await convertInternalToMongodb(internal); + assert.deepStrictEqual(mongodb, { + bsonType: 'object', + properties: { + genres: { + bsonType: 'array', + items: { + bsonType: 'string', + }, + }, + }, + }); + }); + + it('array - complex mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'genres', + path: ['genres'], + count: 1, + type: ['Array', 'Undefined'], + probability: 0.5, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['genres'], + count: 1, + probability: 0.5, + bsonType: 'Array', + types: [ + { + name: 'String', + path: ['genres'], + count: 2, + probability: 0.6666666666666666, + unique: 2, + hasDuplicates: false, + values: ['crimi', 'comedy'], + bsonType: 'String', + }, + { + name: 'Document', + path: ['genres'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'long', + path: ['genres', 'long'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['genres', 'long'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['science fiction'], + bsonType: 'String', + }, + ], + }, + { + name: 'short', + path: ['genres', 'short'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['genres', 'short'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['scifi'], + bsonType: 'String', + }, + ], + }, + ], + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['genres'], + count: 1, + probability: 0.5, + }, + ], + }, + ], + }; + const mongodb = await convertInternalToMongodb(internal); + assert.deepStrictEqual(mongodb, { + bsonType: 'object', + properties: { + genres: { + bsonType: 'array', + items: { + anyOf: [ + { + bsonType: 'string', + }, + { + bsonType: 'object', + required: ['long', 'short'], + properties: { + long: { + bsonType: 'string', + }, + short: { + bsonType: 'string', + }, + }, + }, + ], + }, + }, + }, + }); + }); + + it('array - simple mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'arrayMixedType', + path: ['arrayMixedType'], + count: 1, + type: 'Array', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['arrayMixedType'], + count: 1, + probability: 1, + bsonType: 'Array', + types: [ + { + name: 'int32', + path: ['arrayMixedType'], + count: 2, + probability: 0.6666666666666666, + unique: 2, + hasDuplicates: false, + values: [1, 3], + bsonType: 'Int32', + }, + { + name: 'String', + path: ['arrayMixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: ['2'], + bsonType: 'String', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + ], + }, + ], + }; + const mongodb = await convertInternalToMongodb(internal); + assert.deepStrictEqual(mongodb, { + bsonType: 'object', + required: ['arrayMixedType'], + properties: { + arrayMixedType: { + bsonType: 'array', + items: { + bsonType: ['int', 'string'], + }, + }, + }, + }); + }); + }); + + describe('mixed types', function () { + it('simple mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedType', + path: ['mixedType'], + count: 2, + type: ['Int32', 'String', 'Undefined'], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Int32', + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: [1], + bsonType: 'Int32', + }, + { + name: 'String', + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: ['abc'], + bsonType: 'String', + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + }, + ], + }, + ], + }; + const mongodb = await convertInternalToMongodb(internal); + assert.deepStrictEqual(mongodb, { + bsonType: 'object', + properties: { + mixedType: { + bsonType: ['int', 'string'], + }, + }, + }); + }); + + it('complex mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedComplexType', + path: ['mixedComplexType'], + count: 2, + type: ['Array', 'Document', 'Undefined'], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Array', + types: [ + { + name: 'Int32', + path: ['mixedComplexType'], + count: 3, + probability: 1, + unique: 3, + hasDuplicates: false, + values: [1, 2, 3], + bsonType: 'Int32', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + { + name: 'Document', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'a', + path: ['mixedComplexType', 'a'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['mixedComplexType', 'a'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['bc'], + bsonType: 'String', + }, + ], + }, + ], + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + }, + ], + }, + ], + }; + const mongodb = await convertInternalToMongodb(internal); + assert.deepStrictEqual(mongodb, { + bsonType: 'object', + properties: { + mixedComplexType: { + anyOf: [ + { + bsonType: 'array', + items: { + bsonType: 'int', + }, + }, + { + bsonType: 'object', + required: ['a'], + properties: { + a: { + bsonType: 'string', + }, + }, + }, + ], + }, + }, + }); + }); + }); + + it('can be aborted', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedComplexType', + path: ['mixedComplexType'], + count: 2, + type: ['Array', 'Document', 'Undefined'], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Array', + types: [ + { + name: 'Int32', + path: ['mixedComplexType'], + count: 3, + probability: 1, + unique: 3, + hasDuplicates: false, + values: [1, 2, 3], + bsonType: 'Int32', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + { + name: 'Document', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'a', + path: ['mixedComplexType', 'a'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['mixedComplexType', 'a'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['bc'], + bsonType: 'String', + }, + ], + }, + ], + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + }, + ], + }, + ], + }; + const abortController = new AbortController(); + const promise = convertInternalToMongodb(internal, { + signal: abortController.signal, + }); + abortController.abort(new Error("Too long, didn't wait.")); + await assert.rejects(promise, { + name: 'Error', + message: "Too long, didn't wait.", + }); + }); + }); +}); diff --git a/packages/mongodb-schema/src/schema-converters/internal-to-mongodb.ts b/packages/mongodb-schema/src/schema-converters/internal-to-mongodb.ts new file mode 100644 index 000000000..9ff1dd837 --- /dev/null +++ b/packages/mongodb-schema/src/schema-converters/internal-to-mongodb.ts @@ -0,0 +1,145 @@ +/** + * Transforms the internal schema to $jsonSchema + */ +import type { + ArraySchemaType, + DocumentSchemaType, + Schema as InternalSchema, + SchemaType, +} from '../schema-analyzer'; +import type { MongoDBJSONSchema } from '../types'; +import { allowAbort } from '../util'; + +// The rule's `__proto__: null` fix does not typecheck against `Record`, +// and this map is only ever read with keys derived from our own schema types. +// eslint-disable-next-line @mongodb-js/devtools/no-plain-object-records +export const InternalTypeToBsonTypeMap: Record< + SchemaType['name'] | 'Double' | 'BSONSymbol', + string +> = { + Double: 'double', + Number: 'double', + String: 'string', + Document: 'object', + Array: 'array', + Binary: 'binData', + Undefined: 'undefined', + ObjectId: 'objectId', + Boolean: 'bool', + Date: 'date', + Null: 'null', + RegExp: 'regex', + BSONRegExp: 'regex', + DBRef: 'dbPointer', + BSONSymbol: 'symbol', + Symbol: 'symbol', + Code: 'javascript', + CodeWScope: 'javascriptWithScope', + Int32: 'int', + Timestamp: 'timestamp', + Long: 'long', + Decimal128: 'decimal', + MinKey: 'minKey', + MaxKey: 'maxKey', +}; + +const convertInternalType = (type: string) => { + const bsonType = InternalTypeToBsonTypeMap[type]; + if (!bsonType) throw new Error(`Encountered unknown type: ${type}`); + return bsonType; +}; + +async function parseType( + type: SchemaType, + signal?: AbortSignal, +): Promise { + await allowAbort(signal); + const schema: MongoDBJSONSchema = { + bsonType: convertInternalType(type.bsonType), + }; + + if (type.bsonType === 'Array') { + const items = await parseTypes((type as ArraySchemaType).types); + // Don't include empty bson type arrays (it's invalid). + if (!items.bsonType || items.bsonType?.length > 0) { + schema.items = items; + } + } else if (type.bsonType === 'Document') { + Object.assign( + schema, + await parseFields((type as DocumentSchemaType).fields, signal), + ); + } + + return schema; +} + +function isPlainTypesOnly( + types: MongoDBJSONSchema[], +): types is { bsonType: string }[] { + return types.every( + (definition) => + !!definition.bsonType && Object.keys(definition).length === 1, + ); +} + +async function parseTypes( + types: SchemaType[], + signal?: AbortSignal, +): Promise { + await allowAbort(signal); + const definedTypes = types.filter( + (type) => type.bsonType.toLowerCase() !== 'undefined', + ); + const isSingleType = definedTypes.length === 1; + if (isSingleType) { + return parseType(definedTypes[0], signal); + } + const parsedTypes = await Promise.all( + definedTypes.map((type) => parseType(type, signal)), + ); + if (isPlainTypesOnly(parsedTypes)) { + return { + bsonType: parsedTypes.map(({ bsonType }) => bsonType), + }; + } + return { + anyOf: parsedTypes, + }; +} + +async function parseFields( + fields: DocumentSchemaType['fields'], + signal?: AbortSignal, +): Promise<{ + required?: MongoDBJSONSchema['required']; + properties: MongoDBJSONSchema['properties']; +}> { + const required: string[] = []; + const properties: MongoDBJSONSchema['properties'] = {}; + for (const field of fields) { + if (field.probability === 1) required.push(field.name); + properties[field.name] = await parseTypes(field.types, signal); + } + + return { properties, ...(required.length > 0 ? { required } : {}) }; +} + +export async function convertInternalToMongodb( + internalSchema: InternalSchema, + options: { + signal?: AbortSignal; + } = {}, +): Promise { + const { required, properties } = await parseFields( + internalSchema.fields, + options.signal, + ); + const schema: MongoDBJSONSchema = { + bsonType: 'object', + // Prevent adding an empty required array as it isn't valid. + ...(required === undefined ? {} : { required }), + properties, + }; + return schema; +} diff --git a/packages/mongodb-schema/src/schema-converters/internal-to-standard.test.ts b/packages/mongodb-schema/src/schema-converters/internal-to-standard.test.ts new file mode 100644 index 000000000..bcc85c65b --- /dev/null +++ b/packages/mongodb-schema/src/schema-converters/internal-to-standard.test.ts @@ -0,0 +1,1506 @@ +import assert from 'assert'; +import Ajv2020 from 'ajv/dist/2020'; +import { + convertInternalToStandard, + RELAXED_EJSON_DEFINITIONS, +} from './internal-to-standard'; + +describe('internalSchemaToStandard', function () { + const ajv = new Ajv2020(); + + describe('Converts: ', function () { + it('all the types', async function () { + const internal = { + count: 1, + fields: [ + { + name: '_id', + path: ['_id'], + count: 1, + type: 'ObjectId', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'ObjectId', + path: ['_id'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['642d766b7300158b1f22e972'], + bsonType: 'ObjectId', + }, + ], + }, + { + name: 'array', + path: ['array'], + count: 1, + type: 'Array', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['array'], + count: 1, + probability: 0.8, + bsonType: 'Array', + types: [ + { + name: 'Number', + path: ['array'], + count: 3, + probability: 0.8, + unique: 3, + hasDuplicates: false, + values: [1, 2, 3], + bsonType: 'Number', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + ], + }, + { + name: 'binaries', + path: ['binaries'], + count: 1, + type: 'Document', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: ['binaries'], + count: 1, + probability: 0.8, + bsonType: 'Document', + fields: [ + { + name: 'binaryOld', + path: ['binaries', 'binaryOld'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'binaryOld'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['//8='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'compressedTimeSeries', + path: ['binaries', 'compressedTimeSeries'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'compressedTimeSeries'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['c//SZESzTGmQ6OfR38A11A=='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'custom', + path: ['binaries', 'custom'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'custom'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['//8='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'encrypted', + path: ['binaries', 'encrypted'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'encrypted'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['c//SZESzTGmQ6OfR38A11A=='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'functionData', + path: ['binaries', 'functionData'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'functionData'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['//8='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'generic', + path: ['binaries', 'generic'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'generic'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['AQID'], + bsonType: 'Binary', + }, + ], + }, + { + name: 'md5', + path: ['binaries', 'md5'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'md5'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['c//SZESzTGmQ6OfR38A11A=='], + bsonType: 'Binary', + }, + ], + }, + { + name: 'uuid', + path: ['binaries', 'uuid'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'uuid'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa'], + bsonType: 'Binary', + }, + ], + }, + { + name: 'uuidOld', + path: ['binaries', 'uuidOld'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binaries', 'uuidOld'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['c//SZESzTGmQ6OfR38A11A=='], + bsonType: 'Binary', + }, + ], + }, + ], + }, + ], + }, + { + name: 'binData', + path: ['binData'], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: ['binData'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['AQID'], + bsonType: 'Binary', + }, + ], + }, + { + name: 'boolean', + path: ['boolean'], + count: 1, + type: 'Boolean', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Boolean', + path: ['boolean'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [true], + bsonType: 'Boolean', + }, + ], + }, + { + name: 'date', + path: ['date'], + count: 1, + type: 'Date', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Date', + path: ['date'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['2023-04-05T13:25:08.445Z'], + bsonType: 'Date', + }, + ], + }, + { + name: 'dbRef', + path: ['dbRef'], + count: 1, + type: 'DBRef', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'DBRef', + path: ['dbRef'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $ref: 'namespace', + $id: '642d76b4b7ebfab15d3c4a78', + }, + ], + bsonType: 'DBRef', + }, + ], + }, + { + name: 'decimal', + path: ['decimal'], + count: 1, + type: 'Decimal128', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Decimal128', + path: ['decimal'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $numberDecimal: '5.477284286264328586719275128128001E-4088', + }, + ], + bsonType: 'Decimal128', + }, + ], + }, + { + name: 'double', + path: ['double'], + count: 1, + type: 'Double', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Double', + path: ['double'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [1.2], + bsonType: 'Double', + }, + ], + }, + { + name: 'int', + path: ['int'], + count: 1, + type: 'Int32', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Int32', + path: ['int'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [12345], + bsonType: 'Int32', + }, + ], + }, + { + name: 'javascript', + path: ['javascript'], + count: 1, + type: 'Code', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Code', + path: ['javascript'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + code: 'function() {}', + }, + ], + bsonType: 'Code', + }, + ], + }, + { + name: 'javascriptWithScope', + path: ['javascriptWithScope'], + count: 1, + type: 'CodeWScope', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'CodeWScope', + path: ['javascriptWithScope'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + code: 'function() {}', + scope: { + foo: 1, + bar: 'a', + }, + }, + ], + bsonType: 'CodeWScope', + }, + ], + }, + { + name: 'long', + path: ['long'], + count: 1, + type: 'Long', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Long', + path: ['long'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + low: -1395630315, + high: 28744523, + unsigned: false, + }, + ], + bsonType: 'Long', + }, + ], + }, + { + name: 'maxKey', + path: ['maxKey'], + count: 1, + type: 'MaxKey', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'MaxKey', + path: ['maxKey'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [{}], + bsonType: 'MaxKey', + }, + ], + }, + { + name: 'minKey', + path: ['minKey'], + count: 1, + type: 'MinKey', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'MinKey', + path: ['minKey'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [{}], + bsonType: 'MinKey', + }, + ], + }, + { + name: 'null', + path: ['null'], + count: 1, + type: 'Null', + probability: 0.8, + hasDuplicates: true, + types: [ + { + name: 'Null', + path: ['null'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: true, + bsonType: 'Null', + }, + ], + }, + { + name: 'object', + path: ['object'], + count: 1, + type: 'Document', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: ['object'], + count: 1, + probability: 0.8, + bsonType: 'Document', + fields: [ + { + name: 'key', + path: ['object', 'key'], + count: 1, + type: 'String', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['object', 'key'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['value'], + bsonType: 'String', + }, + ], + }, + ], + }, + ], + }, + { + name: 'objectId', + path: ['objectId'], + count: 1, + type: 'ObjectId', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'ObjectId', + path: ['objectId'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['642d766c7300158b1f22e975'], + bsonType: 'ObjectId', + }, + ], + }, + { + name: 'regex', + path: ['regex'], + count: 1, + type: 'BSONRegExp', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'BSONRegExp', + path: ['regex'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + pattern: 'pattern', + options: 'i', + }, + ], + bsonType: 'BSONRegExp', + }, + ], + }, + { + name: 'string', + path: ['string'], + count: 1, + type: 'String', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['string'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['Hello, world!'], + bsonType: 'String', + }, + ], + }, + { + name: 'symbol', + path: ['symbol'], + count: 1, + type: 'BSONSymbol', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'BSONSymbol', + path: ['symbol'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['symbol'], + bsonType: 'BSONSymbol', + }, + ], + }, + { + name: 'timestamp', + path: ['timestamp'], + count: 1, + type: 'Timestamp', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Timestamp', + path: ['timestamp'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $timestamp: '7218556297505931265', + }, + ], + bsonType: 'Timestamp', + }, + ], + }, + ], + }; + const standard = await convertInternalToStandard(internal); + void ajv.validateSchema(standard); + const expectedDefinitions: any = { + ...RELAXED_EJSON_DEFINITIONS, + }; + delete expectedDefinitions.Undefined; + delete expectedDefinitions.DBPointer; + assert.deepStrictEqual(standard, { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required: [], + $defs: expectedDefinitions, + properties: { + _id: { + $ref: '#/$defs/ObjectId', + }, + array: { + type: 'array', + items: { + $ref: '#/$defs/Double', + }, + }, + binData: { + $ref: '#/$defs/Binary', + }, + binaries: { + type: 'object', + properties: { + binaryOld: { + $ref: '#/$defs/Binary', + }, + compressedTimeSeries: { + $ref: '#/$defs/Binary', + }, + custom: { + $ref: '#/$defs/Binary', + }, + encrypted: { + $ref: '#/$defs/Binary', + }, + functionData: { + $ref: '#/$defs/Binary', + }, + generic: { + $ref: '#/$defs/Binary', + }, + md5: { + $ref: '#/$defs/Binary', + }, + uuid: { + $ref: '#/$defs/Binary', + }, + uuidOld: { + $ref: '#/$defs/Binary', + }, + }, + required: [], + }, + boolean: { + type: 'boolean', + }, + date: { + $ref: '#/$defs/Date', + }, + dbRef: { + $ref: '#/$defs/DBRef', + }, + decimal: { + $ref: '#/$defs/Decimal128', + }, + double: { + $ref: '#/$defs/Double', + }, + int: { + type: 'integer', + }, + javascript: { + $ref: '#/$defs/Code', + }, + javascriptWithScope: { + $ref: '#/$defs/CodeWScope', + }, + long: { + type: 'integer', + }, + maxKey: { + $ref: '#/$defs/MaxKey', + }, + minKey: { + $ref: '#/$defs/MinKey', + }, + null: { + type: 'null', + }, + object: { + type: 'object', + properties: { + key: { + type: 'string', + }, + }, + required: [], + }, + objectId: { + $ref: '#/$defs/ObjectId', + }, + regex: { + $ref: '#/$defs/RegExp', + }, + string: { + type: 'string', + }, + symbol: { + $ref: '#/$defs/BSONSymbol', + }, + timestamp: { + $ref: '#/$defs/Timestamp', + }, + }, + }); + }); + + it('nested document/object', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'author', + path: ['author'], + count: 1, + type: ['Document', 'Undefined'], + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: ['author'], + count: 1, + probability: 0.5, + bsonType: 'Document', + fields: [ + { + name: 'name', + path: ['author', 'name'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['author', 'name'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['Peter Sonder'], + bsonType: 'String', + }, + ], + }, + { + name: 'rating', + path: ['author', 'rating'], + count: 1, + type: 'Double', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Double', + path: ['author', 'rating'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: [1.3], + bsonType: 'Double', + }, + ], + }, + ], + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['author'], + count: 1, + probability: 0.5, + }, + ], + }, + ], + }; + const standard = await convertInternalToStandard(internal); + const expectedDefinitions = { + Double: RELAXED_EJSON_DEFINITIONS.Double, + }; + void ajv.validateSchema(standard); + assert.deepStrictEqual(standard, { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required: ['author'], + $defs: expectedDefinitions, + properties: { + author: { + type: 'object', + required: ['name', 'rating'], + properties: { + name: { + type: 'string', + }, + rating: { + $ref: '#/$defs/Double', + }, + }, + }, + }, + }); + }); + + describe('arrays', function () { + it('array - single type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'genres', + path: ['genres'], + count: 1, + type: ['array', 'Undefined'], + probability: 0.5, + hasDuplicates: false, + types: [ + { + name: 'array', + path: ['genres'], + count: 1, + probability: 0.5, + bsonType: 'Array', + types: [ + { + name: 'String', + path: ['genres'], + count: 2, + probability: 1, + unique: 2, + hasDuplicates: false, + values: ['crimi', 'comedy'], + bsonType: 'String', + }, + ], + totalCount: 2, + lengths: [2], + averageLength: 2, + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['genres'], + count: 1, + probability: 0.5, + }, + ], + }, + ], + }; + const standard = await convertInternalToStandard(internal); + void ajv.validateSchema(standard); + assert.deepStrictEqual(standard, { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required: [], + $defs: {}, + properties: { + genres: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }); + }); + + it('array - complex mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'genres', + path: ['genres'], + count: 1, + type: ['Array', 'Undefined'], + probability: 0.5, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['genres'], + count: 1, + probability: 0.5, + bsonType: 'Array', + types: [ + { + name: 'String', + path: ['genres'], + count: 2, + probability: 0.6666666666666666, + unique: 2, + hasDuplicates: false, + values: ['crimi', 'comedy'], + bsonType: 'String', + }, + { + name: 'Document', + path: ['genres'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'long', + path: ['genres', 'long'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['genres', 'long'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['science fiction'], + bsonType: 'String', + }, + ], + }, + { + name: 'short', + path: ['genres', 'short'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['genres', 'short'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['scifi'], + bsonType: 'String', + }, + ], + }, + ], + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['genres'], + count: 1, + probability: 0.5, + }, + ], + }, + ], + }; + const standard = await convertInternalToStandard(internal); + void ajv.validateSchema(standard); + assert.deepStrictEqual(standard, { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required: [], + $defs: {}, + properties: { + genres: { + type: 'array', + items: { + anyOf: [ + { + type: 'string', + }, + { + type: 'object', + required: ['long', 'short'], + properties: { + long: { + type: 'string', + }, + short: { + type: 'string', + }, + }, + }, + ], + }, + }, + }, + }); + }); + + it('array - simple mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'arrayMixedType', + path: ['arrayMixedType'], + count: 1, + type: 'Array', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['arrayMixedType'], + count: 1, + probability: 1, + bsonType: 'Array', + types: [ + { + name: 'int32', + path: ['arrayMixedType'], + count: 2, + probability: 0.6666666666666666, + unique: 2, + hasDuplicates: false, + values: [1, 3], + bsonType: 'Int32', + }, + { + name: 'String', + path: ['arrayMixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: ['2'], + bsonType: 'String', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + ], + }, + ], + }; + const standard = await convertInternalToStandard(internal); + void ajv.validateSchema(standard); + assert.deepStrictEqual(standard, { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required: ['arrayMixedType'], + $defs: {}, + properties: { + arrayMixedType: { + type: 'array', + items: { + type: ['integer', 'string'], + }, + }, + }, + }); + }); + }); + + describe('mixed types', function () { + it('simple mixed type', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedType', + path: ['mixedType'], + count: 2, + type: ['Int32', 'String', 'Undefined'], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Int32', + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: [1], + bsonType: 'Int32', + }, + { + name: 'String', + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: ['abc'], + bsonType: 'String', + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + }, + ], + }, + ], + }; + const standard = await convertInternalToStandard(internal); + void ajv.validateSchema(standard); + assert.deepStrictEqual(standard, { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required: [], + $defs: {}, + properties: { + mixedType: { + type: ['integer', 'string'], + }, + }, + }); + }); + + it('complex mixed type (with array and object)', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedComplexType', + path: ['mixedComplexType'], + count: 2, + type: ['Array', 'Document', 'Undefined'], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Array', + types: [ + { + name: 'Int32', + path: ['mixedComplexType'], + count: 3, + probability: 1, + unique: 3, + hasDuplicates: false, + values: [1, 2, 3], + bsonType: 'Int32', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + { + name: 'Document', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'a', + path: ['mixedComplexType', 'a'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['mixedComplexType', 'a'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['bc'], + bsonType: 'String', + }, + ], + }, + ], + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + }, + ], + }, + ], + }; + const standard = await convertInternalToStandard(internal); + void ajv.validateSchema(standard); + assert.deepStrictEqual(standard, { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required: [], + $defs: {}, + properties: { + mixedComplexType: { + anyOf: [ + { + type: 'array', + items: { + type: 'integer', + }, + }, + { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'string', + }, + }, + }, + ], + }, + }, + }); + }); + + it('complex mixed type (with $refs)', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedType', + path: ['mixedType'], + count: 2, + type: ['String', 'ObjectId'], + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['mixedType'], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: ['abc'], + bsonType: 'String', + }, + { + name: 'ObjectId', + path: ['objectId'], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: ['642d766c7300158b1f22e975'], + bsonType: 'ObjectId', + }, + ], + }, + ], + }; + const standard = await convertInternalToStandard(internal); + void ajv.validateSchema(standard); + const expectedDefinitions = { + ObjectId: RELAXED_EJSON_DEFINITIONS.ObjectId, + }; + assert.deepStrictEqual(standard, { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required: ['mixedType'], + $defs: expectedDefinitions, + properties: { + mixedType: { + anyOf: [ + { + type: 'string', + }, + { + $ref: '#/$defs/ObjectId', + }, + ], + }, + }, + }); + }); + }); + + it('can be aborted', async function () { + const internal = { + count: 2, + fields: [ + { + name: 'mixedComplexType', + path: ['mixedComplexType'], + count: 2, + type: ['Array', 'Document', 'Undefined'], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Array', + types: [ + { + name: 'Int32', + path: ['mixedComplexType'], + count: 3, + probability: 1, + unique: 3, + hasDuplicates: false, + values: [1, 2, 3], + bsonType: 'Int32', + }, + ], + totalCount: 3, + lengths: [3], + averageLength: 3, + }, + { + name: 'Document', + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'a', + path: ['mixedComplexType', 'a'], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: ['mixedComplexType', 'a'], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: ['bc'], + bsonType: 'String', + }, + ], + }, + ], + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: ['mixedComplexType'], + count: 1, + probability: 0.3333333333333333, + }, + ], + }, + ], + }; + const abortController = new AbortController(); + const promise = convertInternalToStandard(internal, { + signal: abortController.signal, + }); + abortController.abort(new Error("Too long, didn't wait.")); + await assert.rejects(promise, { + name: 'Error', + message: "Too long, didn't wait.", + }); + }); + }); +}); diff --git a/packages/mongodb-schema/src/schema-converters/internal-to-standard.ts b/packages/mongodb-schema/src/schema-converters/internal-to-standard.ts new file mode 100644 index 000000000..6eccee356 --- /dev/null +++ b/packages/mongodb-schema/src/schema-converters/internal-to-standard.ts @@ -0,0 +1,377 @@ +import type { JSONSchema4TypeName } from 'json-schema'; +import type { + ArraySchemaType, + DocumentSchemaType, + Schema as InternalSchema, + SchemaType, +} from '../schema-analyzer'; +import type { StandardJSONSchema } from '../types'; +import { allowAbort } from '../util'; + +type StandardTypeDefinition = + | { type: JSONSchema4TypeName; $ref?: never } + | { $ref: string; type?: never }; + +type TypeToDefinitionMap = Record< + SchemaType['name'] | 'Double' | 'BSONSymbol', + StandardTypeDefinition +>; +export const InternalTypeToStandardTypeMap: TypeToDefinitionMap = { + Double: { $ref: '#/$defs/Double' }, + Number: { $ref: '#/$defs/Double' }, + String: { type: 'string' }, + Document: { type: 'object' }, + Array: { type: 'array' }, + Binary: { $ref: '#/$defs/Binary' }, + Undefined: { $ref: '#/$defs/Undefined' }, + ObjectId: { $ref: '#/$defs/ObjectId' }, + Boolean: { type: 'boolean' }, + Date: { $ref: '#/$defs/Date' }, + Null: { type: 'null' }, + RegExp: { $ref: '#/$defs/RegExp' }, + BSONRegExp: { $ref: '#/$defs/RegExp' }, + DBRef: { $ref: '#/$defs/DBRef' }, + DBPointer: { $ref: '#/$defs/DBPointer' }, + BSONSymbol: { $ref: '#/$defs/BSONSymbol' }, + Code: { $ref: '#/$defs/Code' }, + CodeWScope: { $ref: '#/$defs/CodeWScope' }, + Int32: { type: 'integer' }, + Timestamp: { $ref: '#/$defs/Timestamp' }, + Long: { type: 'integer' }, + Decimal128: { $ref: '#/$defs/Decimal128' }, + MinKey: { $ref: '#/$defs/MinKey' }, + MaxKey: { $ref: '#/$defs/MaxKey' }, +}; + +export const RELAXED_EJSON_DEFINITIONS = { + ObjectId: { + type: 'object', + properties: { + $oid: { + type: 'string', + pattern: '^[0-9a-fA-F]{24}$', + }, + }, + required: ['$oid'], + additionalProperties: false, + }, + BSONSymbol: { + type: 'object', + properties: { + $symbol: { + type: 'string', + }, + }, + required: ['$symbol'], + additionalProperties: false, + }, + Double: { + oneOf: [ + { type: 'number' }, + { + type: 'object', + properties: { + $numberDouble: { + enum: ['Infinity', '-Infinity', 'NaN'], + }, + }, + }, + ], + }, + Decimal128: { + type: 'object', + properties: { + $numberDecimal: { + type: 'string', + }, + }, + required: ['$numberDecimal'], + additionalProperties: false, + }, + Binary: { + type: 'object', + properties: { + $binary: { + type: 'object', + properties: { + base64: { + type: 'string', + }, + subType: { + type: 'string', + pattern: '^[0-9a-fA-F]{1,2}$', // BSON binary type as a one- or two-character hex string + }, + }, + required: ['base64', 'subType'], + additionalProperties: false, + }, + }, + required: ['$binary'], + additionalProperties: false, + }, + Code: { + type: 'object', + properties: { + $code: { + type: 'string', + }, + }, + required: ['$code'], + additionalProperties: false, + }, + CodeWScope: { + type: 'object', + properties: { + $code: { + type: 'string', + }, + $scope: { + type: 'object', + }, + }, + required: ['$code', '$scope'], + additionalProperties: false, + }, + Timestamp: { + type: 'object', + properties: { + $timestamp: { + type: 'object', + properties: { + t: { + type: 'integer', + minimum: 0, + }, + i: { + type: 'integer', + minimum: 0, + }, + }, + required: ['t', 'i'], + additionalProperties: false, + }, + }, + required: ['$timestamp'], + additionalProperties: false, + }, + RegExp: { + type: 'object', + properties: { + $regularExpression: { + type: 'object', + properties: { + pattern: { + type: 'string', + }, + options: { + type: 'string', + pattern: '^[gimuy]*$', + }, + }, + required: ['pattern'], + additionalProperties: false, + }, + }, + required: ['$regularExpression'], + additionalProperties: false, + }, + DBPointer: { + type: 'object', + properties: { + $dbPointer: { + type: 'object', + properties: { + $ref: { + type: 'string', + }, + $id: { + $ref: '#/$defs/ObjectId', + }, + }, + required: ['$ref', '$id'], + additionalProperties: false, + }, + }, + required: ['$dbPointer'], + additionalProperties: false, + }, + Date: { + type: 'object', + properties: { + $date: { + type: 'string', + format: 'date-time', + }, + }, + required: ['$date'], + additionalProperties: false, + }, + DBRef: { + type: 'object', + properties: { + $ref: { + type: 'string', + }, + $id: {}, + $db: { + type: 'string', + }, + }, + required: ['$ref', '$id'], + additionalProperties: true, + }, + MinKey: { + type: 'object', + properties: { + $minKey: { + type: 'integer', + const: 1, + }, + }, + required: ['$minKey'], + additionalProperties: false, + }, + MaxKey: { + type: 'object', + properties: { + $maxKey: { + type: 'integer', + const: 1, + }, + }, + required: ['$maxKey'], + additionalProperties: false, + }, + Undefined: { + type: 'object', + properties: { + $undefined: { + type: 'boolean', + const: true, + }, + }, + required: ['$undefined'], + additionalProperties: false, + }, +}; + +const createConvertInternalToStandard = function () { + const usedDefinitions = new Set(); + + function getUsedDefinitions() { + const filteredDefinitions = Object.fromEntries( + Object.entries(RELAXED_EJSON_DEFINITIONS).filter(([key]) => + usedDefinitions.has(key), + ), + ); + return Object.freeze(filteredDefinitions); + } + + function markUsedDefinition(ref: string) { + usedDefinitions.add(ref.split('/')[2]); + } + + function convertInternalType(internalType: string) { + const type = InternalTypeToStandardTypeMap[internalType]; + if (!type) throw new Error(`Encountered unknown type: ${internalType}`); + return { ...type }; + } + + async function parseType( + type: SchemaType, + signal?: AbortSignal, + ): Promise { + await allowAbort(signal); + const schema: StandardJSONSchema = convertInternalType(type.bsonType); + if (schema.$ref) markUsedDefinition(schema.$ref); + switch (type.bsonType) { + case 'Array': + schema.items = await parseTypes( + (type as ArraySchemaType).types, + signal, + ); + break; + case 'Document': + Object.assign( + schema, + await parseFields((type as DocumentSchemaType).fields, signal), + ); + break; + } + return schema; + } + + function isPlainTypesOnly( + types: StandardJSONSchema[], + ): types is { type: JSONSchema4TypeName }[] { + return types.every( + (definition) => !!definition.type && Object.keys(definition).length === 1, + ); + } + + async function parseTypes( + types: SchemaType[], + signal?: AbortSignal, + ): Promise { + await allowAbort(signal); + const definedTypes = types.filter( + (type) => type.bsonType.toLowerCase() !== 'undefined', + ); + const isSingleType = definedTypes.length === 1; + if (isSingleType) { + return parseType(definedTypes[0], signal); + } + const parsedTypes = await Promise.all( + definedTypes.map((type) => parseType(type, signal)), + ); + if (isPlainTypesOnly(parsedTypes)) { + return { + type: parsedTypes.map(({ type }) => type), + }; + } + return { + anyOf: parsedTypes, + }; + } + + async function parseFields( + fields: DocumentSchemaType['fields'], + signal?: AbortSignal, + ): Promise<{ + required: StandardJSONSchema['required']; + properties: StandardJSONSchema['properties']; + }> { + const required = []; + const properties: StandardJSONSchema['properties'] = {}; + for (const field of fields) { + if (field.probability === 1) required.push(field.name); + properties[field.name] = await parseTypes(field.types, signal); + } + return { required, properties }; + } + + return async function convert( + internalSchema: InternalSchema, + options: { signal?: AbortSignal } = {}, + ): Promise { + const { required, properties } = await parseFields( + internalSchema.fields, + options.signal, + ); + const schema: StandardJSONSchema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + required, + properties, + $defs: getUsedDefinitions(), + }; + return schema; + }; +}; + +export function convertInternalToStandard( + internalSchema: InternalSchema, + options: { signal?: AbortSignal } = {}, +): Promise { + return createConvertInternalToStandard()(internalSchema, options); +} diff --git a/packages/mongodb-schema/src/semantic-types/email.ts b/packages/mongodb-schema/src/semantic-types/email.ts new file mode 100644 index 000000000..d60b3dc9f --- /dev/null +++ b/packages/mongodb-schema/src/semantic-types/email.ts @@ -0,0 +1,7 @@ +// from http://stackoverflow.com/questions/46155/validate-email-address-in-javascript +const emailRegex = + /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/; + +export function isEmail(value: any) { + return emailRegex.test(value); +} diff --git a/packages/mongodb-schema/src/semantic-types/index.ts b/packages/mongodb-schema/src/semantic-types/index.ts new file mode 100644 index 000000000..905b6ede3 --- /dev/null +++ b/packages/mongodb-schema/src/semantic-types/index.ts @@ -0,0 +1,5 @@ +import { isEmail } from './email'; + +export default { + Email: isEmail, +}; diff --git a/packages/mongodb-schema/src/stats.ts b/packages/mongodb-schema/src/stats.ts new file mode 100644 index 000000000..8a6ad3c5b --- /dev/null +++ b/packages/mongodb-schema/src/stats.ts @@ -0,0 +1,105 @@ +import type { + Schema, + ArraySchemaType, + SchemaField, + SchemaType, +} from './schema-analyzer'; + +function widthRecursive(schema?: Schema) { + let width = 0; + if (!schema) { + return width; + } + if (schema.fields === undefined) { + return width; + } + + width += schema.fields.length; + + width += schema.fields + .map((field) => { + const doc = field.types.find((v) => v.name === 'Document'); + return widthRecursive(doc as any); + }) + .reduce((p, c) => p + c || 0, 0); + + width += + schema.fields + .map((field: SchemaField) => { + const arr = field.types.find((v: SchemaType) => v.name === 'Array'); + if (arr) { + const doc = (arr as ArraySchemaType).types.find( + (v) => v.name === 'Document', + ); + return widthRecursive(doc as Schema | undefined); + } + }) + .reduce((p, c) => (p ?? 0) + (c ?? 0) || 0, 0) ?? 0; + return width; +} + +function depthRecursive(schema?: Schema) { + if (!schema) { + return 0; + } + let maxChildDepth = 0; + if (schema.fields !== undefined && schema.fields.length > 0) { + maxChildDepth = + 1 + + Math.max( + Math.max( + ...schema.fields.map((field) => { + const doc = field.types.find((v) => v.name === 'Document'); + return depthRecursive(doc as Schema | undefined); + }), + ), + Math.max( + ...schema.fields.map((field) => { + const arr = field.types.find((v) => v.name === 'Array'); + if (arr) { + const doc = (arr as ArraySchemaType).types.find( + (v) => v.name === 'Document', + ); + return depthRecursive(doc as Schema | undefined); + } + return 0; + }), + ), + ); + } + return maxChildDepth; +} + +function branchingFactors(schema?: Schema) { + const branchArray: any[] = []; + let res; + if (!schema) { + return branchArray; + } + if (schema.fields !== undefined && schema.fields.length > 0) { + branchArray.push(schema.fields.length); + res = schema.fields.map(function (field) { + const doc = field.types.find((v) => v.name === 'Document'); + return branchingFactors(doc as Schema | undefined); + }); + branchArray.push(...res.flat(Infinity)); + res = schema.fields.map(function (field) { + const arr = field.types.find((v) => v.name === 'Array'); + if (arr) { + const doc = (arr as ArraySchemaType).types.find( + (v) => v.name === 'Document', + ); + return branchingFactors(doc as Schema | undefined); + } + return []; + }); + branchArray.push(...res.flat(Infinity)); + } + return branchArray.sort().reverse(); +} + +export default { + width: widthRecursive, + depth: depthRecursive, + branch: branchingFactors, +}; diff --git a/packages/mongodb-schema/src/to-typescript.spec.ts b/packages/mongodb-schema/src/to-typescript.spec.ts new file mode 100644 index 000000000..6b85e83f6 --- /dev/null +++ b/packages/mongodb-schema/src/to-typescript.spec.ts @@ -0,0 +1,281 @@ +import type { MongoDBJSONSchema, StandardJSONSchema } from './types'; +import { analyzeDocuments } from './index'; +import { toTypescriptTypeDefinition } from './to-typescript'; + +import assert from 'assert/strict'; + +import { + BSONRegExp, + Binary, + Code, + DBRef, + Decimal128, + Double, + Int32, + Long, + MaxKey, + MinKey, + ObjectId, + Timestamp, + UUID, + BSONSymbol, +} from 'bson'; + +import { inspect } from 'util'; + +function convertAndCompare(schema: MongoDBJSONSchema, expected: string) { + const code = toTypescriptTypeDefinition(schema); + + try { + assert.equal(code, expected); + } catch (err: any) { + // While the diff you get when this fails is handy, it is much easier to + // just copy/paste the actual code to the expected result once you've + // confirmed it is correct. Also it is nice to be able to see what schema + // you're working with. + + // eslint-disable-next-line no-console + console.log(inspect(schema, { depth: null })); + + // eslint-disable-next-line no-console + console.log(code); + + throw err; + } +} + +describe('toTypescriptTypeDefinition', function () { + it('converts a MongoDB JSON schema to TypeScript', async function () { + const docs = [ + { + _id: new ObjectId('642d766b7300158b1f22e972'), + double: new Double(1.2), // Double, 1, double + doubleThatIsAlsoAnInteger: new Double(1), // Double, 1, double + string: 'Hello, world!', // String, 2, string + object: { key: 'value' }, // Object, 3, object + array: [1, 2, 3], // Array, 4, array + binData: new Binary(Buffer.from([1, 2, 3])), // Binary data, 5, binData + // Undefined, 6, undefined (deprecated) + objectId: new ObjectId('642d766c7300158b1f22e975'), // ObjectId, 7, objectId + boolean: true, // Boolean, 8, boolean + date: new Date('2023-04-05T13:25:08.445Z'), // Date, 9, date + null: null, // Null, 10, null + regex: new BSONRegExp('pattern', 'i'), // Regular Expression, 11, regex + // DBPointer, 12, dbPointer (deprecated) + javascript: new Code('function() {}'), // JavaScript, 13, javascript + symbol: new BSONSymbol('symbol'), // Symbol, 14, symbol (deprecated) + javascriptWithScope: new Code('function() {}', { foo: 1, bar: 'a' }), // JavaScript code with scope 15 "javascriptWithScope" Deprecated in MongoDB 4.4. + int: new Int32(12345), // 32-bit integer, 16, "int" + timestamp: new Timestamp(new Long('7218556297505931265')), // Timestamp, 17, timestamp + long: new Long('123456789123456789'), // 64-bit integer, 18, long + decimal: new Decimal128( + Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), + ), // Decimal128, 19, decimal + minKey: new MinKey(), // Min key, -1, minKey + maxKey: new MaxKey(), // Max key, 127, maxKey + + binaries: { + generic: new Binary(Buffer.from([1, 2, 3]), 0), // 0 + functionData: new Binary(Buffer.from('//8='), 1), // 1 + binaryOld: new Binary(Buffer.from('//8='), 2), // 2 + uuidOld: new Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 3), // 3 + uuid: new UUID('AAAAAAAA-AAAA-4AAA-AAAA-AAAAAAAAAAAA'), // 4 + md5: new Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 5), // 5 + encrypted: new Binary(Buffer.from('c//SZESzTGmQ6OfR38A11A=='), 6), // 6 + compressedTimeSeries: new Binary( + Buffer.from( + 'CQCKW/8XjAEAAIfx//////////H/////////AQAAAAAAAABfAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAHAAAAAAAAAA4AAAAAAAAAAA==', + 'base64', + ), + 7, + ), // 7 + custom: new Binary(Buffer.from('//8='), 128), // 128 + }, + + dbRef: new DBRef('namespace', new ObjectId('642d76b4b7ebfab15d3c4a78')), // not actually a separate type, just a convention + + // TODO: what about arrays of objects or arrays of arrays or heterogynous types in general + }, + ]; + const analyzedDocuments = await analyzeDocuments(docs); + const schema = await analyzedDocuments.getMongoDBJsonSchema(); + + convertAndCompare( + schema, + `{ + _id?: bson.ObjectId; + array?: bson.Double | number)[]; + binaries?: { + binaryOld?: bson.Binary; + compressedTimeSeries?: bson.Binary; + custom?: bson.Binary; + encrypted?: bson.Binary; + functionData?: bson.Binary; + generic?: bson.Binary; + md5?: bson.Binary; + uuid?: bson.Binary; + uuidOld?: bson.Binary; + }; + binData?: bson.Binary; + boolean?: boolean; + date?: bson.Date; + dbRef?: bson.DBPointer; + decimal?: bson.Decimal128; + double?: bson.Double | number; + doubleThatIsAlsoAnInteger?: bson.Double | number; + int?: bson.Int32 | number; + javascript?: bson.Code; + javascriptWithScope?: bson.Code; + long?: bson.Long | number; + maxKey?: bson.MaxKey; + minKey?: bson.MinKey; + null?: null; + object?: { + key?: string; + }; + objectId?: bson.ObjectId; + regex?: bson.BSONRegExp; + string?: string; + symbol?: bson.BSONSymbol; + timestamp?: bson.Timestamp; +}`, + ); + }); + + it('converts a standard JSON schema to TypeScript', function () { + // from https://json-schema.org/learn/miscellaneous-examples#complex-object-with-nested-properties + const schema: StandardJSONSchema = { + $id: 'https://example.com/complex-object.schema.json', + $schema: 'https://json-schema.org/draft/2020-12/schema', + title: 'Complex Object', + type: 'object', + properties: { + name: { + type: 'string', + }, + age: { + type: 'integer', + minimum: 0, + }, + address: { + type: 'object', + properties: { + street: { + type: 'string', + }, + city: { + type: 'string', + }, + state: { + type: 'string', + }, + postalCode: { + type: 'string', + pattern: '\\d{5}', + }, + }, + required: ['street', 'city', 'state', 'postalCode'], + }, + hobbies: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['name', 'age'], + }; + + convertAndCompare( + schema, + `{ + name?: string; + age?: number; + address?: { + street?: string; + city?: string; + state?: string; + postalCode?: string; + }; + hobbies?: string[]; +}`, + ); + }); + + it('deals with inconsistent types', async function () { + const docs = [ + { + a: 1, + }, + { + a: 'foo', + }, + { + a: true, + }, + { + a: null, + }, + ]; + + const analyzedDocuments = await analyzeDocuments(docs); + const schema = await analyzedDocuments.getMongoDBJsonSchema(); + + convertAndCompare( + schema, + `{ + a?: bson.Double | number | string | boolean | null; +}`, + ); + }); + + it('deals with nested arrays', async function () { + const docs = [ + { + a: [['foo']], + b: [[{ b: 'foo' }]], + }, + ]; + + const analyzedDocuments = await analyzeDocuments(docs); + const schema = await analyzedDocuments.getMongoDBJsonSchema(); + + convertAndCompare( + schema, + `{ + a?: any[]; + b?: any[]; +}`, + ); + }); + + it('deals with nested objects', async function () { + const docs = [ + { + a: { + foo: { bar: 'baz' }, + }, + b: [ + { + foo: { bar: 'baz' }, + }, + ], + }, + ]; + + const analyzedDocuments = await analyzeDocuments(docs); + const schema = await analyzedDocuments.getMongoDBJsonSchema(); + + convertAndCompare( + schema, + `{ + a?: { + foo?: { + bar?: string; + }; + }; + b?: any[]; +}`, + ); + }); +}); diff --git a/packages/mongodb-schema/src/to-typescript.ts b/packages/mongodb-schema/src/to-typescript.ts new file mode 100644 index 000000000..7b2c181e6 --- /dev/null +++ b/packages/mongodb-schema/src/to-typescript.ts @@ -0,0 +1,147 @@ +import type { JSONSchema } from './types'; + +function getBSONType(property: JSONSchema): string | string[] | undefined { + return property.bsonType || property.type; +} + +function assertIsDefined( + value: T, + message: string, +): asserts value is NonNullable { + if (value === undefined || value === null) { + throw new Error(message); + } +} + +function toTypeName(type: string): string | string[] { + switch (type) { + // JSON Schema types + case 'string': + return 'string'; + + case 'number': + case 'integer': + return 'number'; + + case 'boolean': + return 'boolean'; + + case 'null': + return 'null'; + + // BSON types + // see InternalTypeToBsonTypeMap in mongodb-schema: + // https://github.com/mongodb-js/mongodb-schema/blob/5ca185a6967e0f0d1bb20f75555d3f4f1f9c24fe/src/schema-converters/internalToMongoDB.ts#L8 + case 'double': + return ['bson.Double', 'number']; + + case 'binData': + return 'bson.Binary'; + case 'objectId': + return 'bson.ObjectId'; + + case 'bool': + return 'boolean'; + + case 'date': + return 'bson.Date'; + + case 'regex': + return 'bson.BSONRegExp'; + + case 'symbol': + return 'bson.BSONSymbol'; + + case 'javascript': + case 'javascriptWithScope': + return 'bson.Code'; + + case 'int': + return ['bson.Int32', 'number']; + + case 'timestamp': + return 'bson.Timestamp'; + + case 'long': + return ['bson.Long', 'number']; + + case 'decimal': + return 'bson.Decimal128'; + + case 'minKey': + return 'bson.MinKey'; + + case 'maxKey': + return 'bson.MaxKey'; + + case 'dbPointer': + return 'bson.DBPointer'; + + case 'undefined': + return 'undefined'; + + default: + return 'any'; + } +} + +function uniqueTypes(property: JSONSchema): Set { + const type = getBSONType(property); + const types = (Array.isArray(type) ? type : [type ?? 'any']) + .map((t) => toTypeName(t)) + .flat(); + return new Set(types.flat()); +} + +function indentSpaces(indent: number) { + const spaces = []; + for (let i = 0; i < indent; i++) { + spaces.push(' '); + } + return spaces.join(''); +} + +function arrayType(types: string[]) { + if (types.length === 1) { + return `${types[0]}[]`; + } + return `${types.join(' | ')})[]`; +} + +function toTypescriptType( + properties: Record, + indent: number, +): string { + const eachFieldDefinition = Object.entries(properties).map( + ([propertyName, schema]) => { + switch (getBSONType(schema)) { + case 'array': + assertIsDefined(schema.items, 'schema.items must be defined'); + return `${indentSpaces(indent)}${propertyName}?: ${arrayType([ + ...uniqueTypes(schema.items), + ])}`; + case 'object': + assertIsDefined( + schema.properties, + 'schema.properties must be defined', + ); + return `${indentSpaces(indent)}${propertyName}?: ${toTypescriptType( + schema.properties as Record, + indent + 1, + )}`; + default: + return `${indentSpaces(indent)}${propertyName}?: ${[ + ...uniqueTypes(schema), + ].join(' | ')}`; + } + }, + ); + + return `{\n${eachFieldDefinition.join(';\n')};\n${indentSpaces(indent - 1)}}`; +} + +export function toTypescriptTypeDefinition(schema: JSONSchema): string { + assertIsDefined(schema.properties, 'schema.properties must be defined'); + + return toTypescriptType(schema.properties as Record, 1); +} diff --git a/packages/mongodb-schema/src/types.ts b/packages/mongodb-schema/src/types.ts new file mode 100644 index 000000000..b69cb152f --- /dev/null +++ b/packages/mongodb-schema/src/types.ts @@ -0,0 +1,44 @@ +import { type JSONSchema4 } from 'json-schema'; +import type { InternalSchema } from '.'; + +export type StandardJSONSchema = JSONSchema4; + +export type MongoDBJSONSchema = Pick< + StandardJSONSchema, + 'title' | 'required' | 'description' +> & { + bsonType?: string | string[]; + properties?: Record; + items?: MongoDBJSONSchema | MongoDBJSONSchema[]; + anyOf?: MongoDBJSONSchema[]; +}; + +export type ExpandedJSONSchema = StandardJSONSchema & { + ['x-bsonType']?: string | string[]; + ['x-metadata']?: { + hasDuplicates?: boolean; + probability: number; + count: number; + }; + ['x-sampleValues']?: any[]; + properties?: Record; + items?: ExpandedJSONSchema | ExpandedJSONSchema[]; + anyOf?: ExpandedJSONSchema[]; +}; + +export type JSONSchema = Partial & MongoDBJSONSchema; + +export type AnyIterable = Iterable | AsyncIterable; + +type AnySchema = + | InternalSchema + | StandardJSONSchema + | MongoDBJSONSchema + | ExpandedJSONSchema; +export type SchemaConverterFn< + InputSchema = AnySchema, + OutputSchema = AnySchema, +> = ( + input: InputSchema, + options: { signal?: AbortSignal }, +) => Promise; diff --git a/packages/mongodb-schema/src/util.ts b/packages/mongodb-schema/src/util.ts new file mode 100644 index 000000000..cf5e17a24 --- /dev/null +++ b/packages/mongodb-schema/src/util.ts @@ -0,0 +1,11 @@ +export const ALLOW_ABORT_INTERVAL_COUNT = 1000; + +export async function allowAbort(signal?: AbortSignal) { + return new Promise((resolve, reject) => + setTimeout(() => { + if (signal?.aborted) + return reject(signal?.reason || new Error('Operation aborted')); + resolve(); + }), + ); +} diff --git a/packages/mongodb-schema/test/all-bson-types-fixture.ts b/packages/mongodb-schema/test/all-bson-types-fixture.ts new file mode 100644 index 000000000..87d55ee40 --- /dev/null +++ b/packages/mongodb-schema/test/all-bson-types-fixture.ts @@ -0,0 +1,79 @@ +import { + BSONRegExp, + Binary, + Code, + DBRef, + Decimal128, + Double, + Int32, + Long, + MaxKey, + MinKey, + ObjectId, + Timestamp, + UUID, + BSONSymbol, +} from 'bson'; + +export const allBSONTypesDoc = { + _id: new ObjectId('642d766b7300158b1f22e972'), + double: new Double(1.2), // Double, 1, double + string: 'Hello, world!', // String, 2, string + object: { key: 'value' }, // Object, 3, object + array: [1, 2, 3], // Array, 4, array + binData: new Binary(Buffer.from([1, 2, 3])), // Binary data, 5, binData + // Undefined, 6, undefined (deprecated) + objectId: new ObjectId('642d766c7300158b1f22e975'), // ObjectId, 7, objectId + boolean: true, // Boolean, 8, boolean + date: new Date('2023-04-05T13:25:08.445Z'), // Date, 9, date + null: null, // Null, 10, null + regex: new BSONRegExp('pattern', 'i'), // Regular Expression, 11, regex + // DBPointer, 12, dbPointer (deprecated) + javascript: new Code('function() {}'), // JavaScript, 13, javascript + symbol: new BSONSymbol('symbol'), // Symbol, 14, symbol (deprecated) + javascriptWithScope: new Code('function() {}', { foo: 1, bar: 'a' }), // JavaScript code with scope 15 "javascriptWithScope" Deprecated in MongoDB 4.4. + int: new Int32(12345), // 32-bit integer, 16, "int" + timestamp: new Timestamp(new Long('7218556297505931265')), // Timestamp, 17, timestamp + long: new Long('123456789123456789'), // 64-bit integer, 18, long + decimal: new Decimal128( + Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), + ), // Decimal128, 19, decimal + minKey: new MinKey(), // Min key, -1, minKey + maxKey: new MaxKey(), // Max key, 127, maxKey + + binaries: { + generic: new Binary(Buffer.from([1, 2, 3]), 0), // 0 + functionData: Binary.createFromBase64('//8=', 1), // 1 + binaryOld: Binary.createFromBase64('//8=', 2), // 2 + uuidOld: Binary.createFromBase64('c//SZESzTGmQ6OfR38A11A==', 3), // 3 + uuid: new UUID('AAAAAAAA-AAAA-4AAA-AAAA-AAAAAAAAAAAA'), // 4 + md5: Binary.createFromBase64('c//SZESzTGmQ6OfR38A11A==', 5), // 5 + encrypted: Binary.createFromBase64('c//SZESzTGmQ6OfR38A11A==', 6), // 6 + compressedTimeSeries: new Binary( + Buffer.from( + 'CQCKW/8XjAEAAIfx//////////H/////////AQAAAAAAAABfAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAHAAAAAAAAAA4AAAAAAAAAAA==', + 'base64', + ), + 7, + ), // 7 + custom: Binary.createFromBase64('//8=', 128), // 128 + }, + + dbRef: new DBRef('namespace', new ObjectId('642d76b4b7ebfab15d3c4a78')), // not actually a separate type, just a convention +}; + +const { dbRef, ...allValidBSONTypesDoc } = allBSONTypesDoc; + +// Includes some edge cases like empty objects, nested empty arrays, etc. +export const allValidBSONTypesWithEdgeCasesDoc = { + ...allValidBSONTypesDoc, + emptyObject: {}, + objectWithNestedEmpty: { + nestedEmpty: {}, + }, + emptyArray: [], + arrayOfEmptyArrays: [[], []], + infinityNum: Infinity, + negativeInfinityNum: -Infinity, + NaNNum: NaN, +}; diff --git a/packages/mongodb-schema/test/all-bson-types.test.ts b/packages/mongodb-schema/test/all-bson-types.test.ts new file mode 100644 index 000000000..cf0357546 --- /dev/null +++ b/packages/mongodb-schema/test/all-bson-types.test.ts @@ -0,0 +1,46 @@ +import assert from 'assert'; + +import type { Schema } from '../src/schema-analyzer'; +import getSchema from '../src'; +import { allBSONTypesDoc } from './all-bson-types-fixture'; + +describe('using a document with all bson types', function () { + let schema: Schema; + before(async function () { + schema = await getSchema([allBSONTypesDoc]); + }); + + it('contains all of the types', function () { + assert.equal(schema.count, 1); + + const fieldTypes = [ + 'Array', + 'Binary', + 'Boolean', + 'Code', + 'CodeWScope', + 'Date', + 'Decimal128', + 'Double', + 'Int32', + 'Long', + 'MaxKey', + 'MinKey', + 'Null', + 'Document', + 'ObjectId', + 'BSONRegExp', + 'String', + 'BSONSymbol', + 'Timestamp', + ]; + + for (const type of fieldTypes) { + assert.equal( + !!schema.fields.find((schemaField) => schemaField.type === type), + true, + `Cannot find type "${type}" in schemaField types: ${JSON.stringify(schema.fields, null, 2)}`, + ); + } + }); +}); diff --git a/packages/mongodb-schema/test/array-object-types.test.ts b/packages/mongodb-schema/test/array-object-types.test.ts new file mode 100644 index 000000000..470b57390 --- /dev/null +++ b/packages/mongodb-schema/test/array-object-types.test.ts @@ -0,0 +1,104 @@ +import getSchema from '../src'; +import assert from 'assert'; +import type { + ArraySchemaType, + Schema, + SchemaField, + SchemaType, +} from '../src/schema-analyzer'; + +describe('arrays and objects as type (INT-203 restructuring)', function () { + const docs = [ + { + x: [1, 2, 3], + }, + { + x: 'foo', + }, + { + x: { + b: 1, + }, + }, + { + x: ['bar', null, false], + }, + { + x: [ + { + c: 1, + d: 1, + }, + { + c: 2, + }, + ], + }, + { + e: 1, + }, + ]; + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + describe('Field', function () { + let x: SchemaField | undefined; + before(function () { + x = schema.fields.find((v) => v.name === 'x'); + }); + it('have the right type distribution of x', function () { + const names = x?.types.map((v) => v.name); + const probabilities = x?.types.map((v) => v.probability); + const dist = names?.reduce( + (p, c, i) => ({ ...p, [c]: probabilities?.[i] }), + {}, + ); + assert.deepEqual(dist, { + Array: 3 / 6, + String: 1 / 6, + Document: 1 / 6, + Undefined: 1 / 6, + }); + }); + }); + + describe('Nested Array', function () { + let arr: SchemaType | undefined; + + before(function () { + const types = schema.fields.find((v) => v.name === 'x')?.types; + arr = types?.find((v) => v.name === 'Array'); + }); + + it('should return the lengths of all encountered arrays', function () { + assert.deepEqual((arr as ArraySchemaType).lengths, [3, 3, 2]); + }); + + it('should return the probability of x being an array', function () { + assert.equal(arr?.probability, 3 / 6); + }); + + it('should return the total count of all containing values', function () { + assert.equal((arr as ArraySchemaType).totalCount, 8); + }); + + it('should return the type distribution inside an array', function () { + const names = (arr as ArraySchemaType).types.map((v) => v.name); + const probabilities = (arr as ArraySchemaType).types.map( + (v) => v.probability, + ); + const arrDist = names.reduce( + (p, c, i) => ({ ...p, [c]: probabilities[i] }), + {}, + ); + assert.deepEqual(arrDist, { + Number: 3 / 8, + String: 1 / 8, + Null: 1 / 8, + Boolean: 1 / 8, + Document: 2 / 8, + }); + }); + }); +}); diff --git a/packages/mongodb-schema/test/basic-embedded-array.test.ts b/packages/mongodb-schema/test/basic-embedded-array.test.ts new file mode 100644 index 000000000..de2004912 --- /dev/null +++ b/packages/mongodb-schema/test/basic-embedded-array.test.ts @@ -0,0 +1,57 @@ +import assert from 'assert'; +import BSON from 'bson'; + +import getSchema from '../src'; +import type { ArraySchemaType } from '../src/schema-analyzer'; + +describe('basic embedded array', function () { + let followingIds: ArraySchemaType; + const docs = [ + { + _id: new BSON.ObjectId('55581e0a9bf712d0c2b48d71'), + followingIds: [new BSON.ObjectId('55582407aafa8fbbc57196e2')], + }, + { + _id: new BSON.ObjectId('55582407aafa8fbbc57196e2'), + followingIds: [ + new BSON.ObjectId('55581e0a9bf712d0c2b48d71'), + '55581e0a9bf712d0c2b48d71', + ], + }, + ]; + + before(async function () { + const res = await getSchema(docs); + const types = res.fields.find((v) => v.name === 'followingIds')?.types; + followingIds = types?.find((v) => v.name === 'Array') as ArraySchemaType; + }); + + it('should have 2 lengths for followingIds', function () { + assert.deepEqual(followingIds.lengths, [1, 2]); + }); + + it('should have an average length of 1.5 for followingIds', function () { + assert.equal(followingIds.averageLength, 1.5); + }); + + it('should have a sum of probability for followingIds of 1', function () { + const expectedSum = followingIds.types + .map((v) => v.probability) + .reduce((p, c) => p + c || 0, 0); + assert.equal(expectedSum, 1); + }); + + it('should have 33% String for followingIds', function () { + assert.equal( + followingIds.types.find((v) => v.name === 'String')?.probability, + 1 / 3, + ); + }); + + it('should have 66% ObjectId for followingIds', function () { + assert.equal( + followingIds.types.find((v) => v.name === 'ObjectId')?.probability, + 2 / 3, + ); + }); +}); diff --git a/packages/mongodb-schema/test/basic-embedded-documents.test.ts b/packages/mongodb-schema/test/basic-embedded-documents.test.ts new file mode 100644 index 000000000..ffa29caec --- /dev/null +++ b/packages/mongodb-schema/test/basic-embedded-documents.test.ts @@ -0,0 +1,75 @@ +import assert from 'assert'; +import BSON from 'bson'; + +import getSchema from '../src'; +import type { + Schema, + DocumentSchemaType, + SchemaField, +} from '../src/schema-analyzer'; + +describe('basic embedded documents', function () { + const docs = [ + { + _id: new BSON.ObjectId('55582407aafa8fbbc57196e2'), + name: 'Brett Flowers', + email: { + _id: 'gohu@pum.io', + is_verified: false, + }, + twitter: { + username: '@lekbisova', + }, + stats: { + friends: 7584, + }, + push_token: { + android: undefined, + apple: + '4e2e068cd281cfe924ff3174dfe363bd3108a5852ca5197f37c40c1bca6e1a4c', + }, + last_address: { + latitude: null, + longitude: null, + }, + created_at: new Date(), + }, + ]; + + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should detect all fields names and nested paths', function () { + const fieldNames = [ + '_id', + 'created_at', + 'email', + 'last_address', + 'name', + 'push_token', + 'stats', + 'twitter', + ]; + + const nestedFieldPaths = [ + ['push_token', 'android'], + ['push_token', 'apple'], + ]; + + assert.deepEqual( + schema.fields.map((v) => v.name), + fieldNames, + ); + + const types = schema.fields.find((v) => v.name === 'push_token')?.types; + const pushTokens = ( + types?.find((v) => v.name === 'Document') as DocumentSchemaType + )?.fields; + assert.deepEqual( + pushTokens?.map((v: SchemaField) => v.path), + nestedFieldPaths, + ); + }); +}); diff --git a/packages/mongodb-schema/test/basic-probability.test.ts b/packages/mongodb-schema/test/basic-probability.test.ts new file mode 100644 index 000000000..12163b37b --- /dev/null +++ b/packages/mongodb-schema/test/basic-probability.test.ts @@ -0,0 +1,36 @@ +import assert from 'assert'; + +import getSchema from '../src'; +import type { Schema } from '../src/schema-analyzer'; + +describe('simple probability', function () { + const docs = [ + { + _id: 1, + registered: true, + }, + { + _id: 2, + }, + ]; + + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + + if (!schema.fields.find((v) => v.name === 'registered')) { + throw new Error('Did not pick up `registered` field'); + } + }); + it('should have a probability of 50% for `registered` to be a boolean', function () { + const types = schema.fields.find((v) => v.name === 'registered')?.types; + assert.equal(types?.find((v) => v.name === 'Boolean')?.probability, 1 / 2); + }); + it('should have a probability of 50% for `registered` to be undefined', function () { + const types = schema.fields.find((v) => v.name === 'registered')?.types; + assert.equal( + types?.find((v) => v.name === 'Undefined')?.probability, + 1 / 2, + ); + }); +}); diff --git a/packages/mongodb-schema/test/basic-unique.test.ts b/packages/mongodb-schema/test/basic-unique.test.ts new file mode 100644 index 000000000..533a5f22a --- /dev/null +++ b/packages/mongodb-schema/test/basic-unique.test.ts @@ -0,0 +1,134 @@ +/* eslint-disable mocha/max-top-level-suites */ +import assert from 'assert'; +import bson from 'bson'; + +import getSchema from '../src'; +import type { Schema, PrimitiveSchemaType } from '../src/schema-analyzer'; + +describe('hasDuplicates', function () { + const docs: { + num: number; + str: string; + }[] = []; + for (let i = 0; i <= 11111; i++) { + docs.push({ + num: i, + str: String(i), + }); + } + + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should not have duplicates', function () { + assert.equal( + schema.fields.find((v) => v.name === 'num')?.hasDuplicates, + false, + ); + }); + + it('should have 10000 number values for the `num` field', function () { + const types = schema.fields.find((v) => v.name === 'num')?.types; + assert.equal( + (types?.find((v) => v.name === 'Number') as PrimitiveSchemaType)?.values + .length, + 10000, + ); + }); + + it('should have 100 string values for the `str` field', function () { + const types = schema.fields.find((v) => v.name === 'str')?.types; + assert.equal( + (types?.find((v) => v.name === 'String') as PrimitiveSchemaType)?.values + .length, + 100, + ); + }); +}); + +describe('unique', function () { + const docs = [ + { + _id: 1, + registered: true, + b: false, + int32: new bson.Int32(5), + date: new Date('2016-01-01'), + }, + { + _id: 2, + registered: true, + code: null, + b: 'false', + int32: new bson.Int32(5), + date: new Date('2016-01-01'), + }, + { + _id: 3, + code: null, + int32: new bson.Int32(9), + date: new Date('2011-11-11'), + }, + ]; + + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should have count of 3 for `_id`', function () { + assert.equal(schema.fields.find((v) => v.name === '_id')?.count, 3); + }); + + it('should have unique of 3 for `_id`', function () { + const types = schema.fields.find((v) => v.name === '_id')?.types; + assert.equal(types?.find((v) => v.name === 'Number')?.unique, 3); + }); + + it('should not have duplicates for `_id`', function () { + assert.equal( + schema.fields.find((v) => v.name === '_id')?.hasDuplicates, + false, + ); + }); + + it('should have count of 2 for `registered`', function () { + assert.equal(schema.fields.find((v) => v.name === 'registered')?.count, 2); + }); + + it('should have unique of 1 for `registered` type Boolean', function () { + const types = schema.fields.find((v) => v.name === 'registered')?.types; + assert.equal(types?.find((v) => v.name === 'Boolean')?.unique, 1); + }); + + it('should have unique of 1 for `code`', function () { + const types = schema.fields.find((v) => v.name === 'code')?.types; + assert.equal(types?.find((v) => v.name === 'Null')?.unique, 1); + }); + + it('should have unique of 2 for `int32`', function () { + const types = schema.fields.find((v) => v.name === 'int32')?.types; + assert.equal(types?.find((v) => v.name === 'Int32')?.unique, 2); + }); + + it('should have unique of 2 for `date`', function () { + const types = schema.fields.find((v) => v.name === 'date')?.types; + assert.equal(types?.find((v) => v.name === 'Date')?.unique, 2); + }); + + it('should not have duplicate values for b', function () { + assert.equal( + schema.fields.find((v) => v.name === 'b')?.hasDuplicates, + false, + ); + }); + + it('should have duplicates for `registered`', function () { + assert.equal( + schema.fields.find((v) => v.name === 'registered')?.hasDuplicates, + true, + ); + }); +}); diff --git a/packages/mongodb-schema/test/basic.test.ts b/packages/mongodb-schema/test/basic.test.ts new file mode 100644 index 000000000..ab28cb3a9 --- /dev/null +++ b/packages/mongodb-schema/test/basic.test.ts @@ -0,0 +1,91 @@ +import assert from 'assert'; +import BSON from 'bson'; + +import type { Schema } from '../src/schema-analyzer'; +import getSchema from '../src'; + +describe('using only basic fields', function () { + const docs = [ + { + _id: new BSON.ObjectId('55581e0a9bf712d0c2b48d71'), + email: 'tupjud@weigehib.gov', + is_verified: false, + twitter_username: '@zaetisi', + name: 'Hunter Maxwell', + stats_friends: 2163, + apple_push_token: + 'd4b4e7f3361cec05fae848575d7e6e1da2f0dccdf8ccc86a8ff2124d8b0542f6', + android_push_token: undefined, + last_address_latitude: null, + last_address_longitude: null, + created_at: new Date(), + length: 29, + 'name[]': 'Annabeth Frankie', + toString: 42, + }, + ]; + + let users: Schema; + it('should detect all fields', function () { + const fieldNames = [ + '_id', + 'android_push_token', + 'apple_push_token', + 'created_at', + 'email', + 'is_verified', + 'last_address_latitude', + 'last_address_longitude', + 'length', + 'name', + 'stats_friends', + 'twitter_username', + 'name[]', + 'toString', + ]; + assert.deepEqual(users.fields.map((v) => v.name).sort(), fieldNames.sort()); + }); + + before(async function () { + users = await getSchema(docs); + }); + + it('should detect the correct type for each field', function () { + assert.equal(users.fields.find((v) => v.name === '_id')?.type, 'ObjectId'); + assert.equal( + users.fields.find((v) => v.name === 'apple_push_token')?.type, + 'String', + ); + assert.equal( + users.fields.find((v) => v.name === 'created_at')?.type, + 'Date', + ); + assert.equal(users.fields.find((v) => v.name === 'email')?.type, 'String'); + assert.equal( + users.fields.find((v) => v.name === 'is_verified')?.type, + 'Boolean', + ); + assert.equal(users.fields.find((v) => v.name === 'length')?.type, 'Number'); + assert.equal( + users.fields.find((v) => v.name === 'last_address_latitude')?.type, + 'Null', + ); + assert.equal( + users.fields.find((v) => v.name === 'last_address_longitude')?.type, + 'Null', + ); + assert.equal(users.fields.find((v) => v.name === 'name')?.type, 'String'); + assert.equal( + users.fields.find((v) => v.name === 'stats_friends')?.type, + 'Number', + ); + assert.equal( + users.fields.find((v) => v.name === 'twitter_username')?.type, + 'String', + ); + assert.equal( + users.fields.find((v) => v.name === 'toString')?.type, + 'Number', + ); + }); +}); diff --git a/packages/mongodb-schema/test/bloated.test.ts b/packages/mongodb-schema/test/bloated.test.ts new file mode 100644 index 000000000..eeec6d347 --- /dev/null +++ b/packages/mongodb-schema/test/bloated.test.ts @@ -0,0 +1,142 @@ +import assert from 'assert'; +import { Binary, Code } from 'bson'; + +import type { PrimitiveSchemaType } from '../src/schema-analyzer'; +import getSchema from '../src'; + +function generateRandomString(length: number) { + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +describe('bloated documents', function () { + describe('sizeable sample values', function () { + it('really long string is cropped', async function () { + const documents = [ + { + str: generateRandomString(20000), + }, + ]; + const schema = await getSchema(documents); + const stringLength = ( + (schema.fields[0].types[0] as PrimitiveSchemaType).values[0] as string + ).length; + assert.ok(stringLength <= 10000); + }); + + it('really long code is cropped', async function () { + const documents = [ + { + code: new Code(generateRandomString(20000)), + }, + ]; + const schema = await getSchema(documents); + const codeLength = ( + (schema.fields[0].types[0] as PrimitiveSchemaType).values[0] as Code + ).code.length; + assert.ok(codeLength <= 10000); + }); + + it('really long binary is cropped', async function () { + const documents = [ + { + binData: new Binary(Buffer.from(generateRandomString(20000)), 2), + }, + ]; + const schema = await getSchema(documents); + const binary = (schema.fields[0].types[0] as PrimitiveSchemaType) + .values[0] as Binary; + assert.ok(binary.length() <= 10000); + assert.strictEqual(binary.sub_type, 2); + }); + + it('the limit is configurable', async function () { + const documents = [ + { + str: generateRandomString(20000), + }, + ]; + const schema = await getSchema(documents, { storedValuesLengthLimit: 5 }); + const stringLength = ( + (schema.fields[0].types[0] as PrimitiveSchemaType).values[0] as string + ).length; + assert.ok(stringLength === 5); + }); + }); + + describe('high complexity', function () { + it('aborts after reaching the given limit', async function () { + const documents = [ + { + field1: 'abc', + field2: 'bca', + field3: 'cba', + field4: 'cab', + field5: 'bac', + }, + ]; + try { + await getSchema(documents, { distinctFieldsAbortThreshold: 4 }); + assert.fail('Analysis did not throw'); + } catch (error) { + assert.strictEqual( + (error as Error).message, + 'Schema analysis aborted: Fields count above 4', + ); + } + }); + + it('aborts after reaching the given limit - nested', async function () { + const documents = [ + { + field1: { + field2: { + field3: 'abc', + field4: 'bca', + }, + field5: 'cab', + }, + }, + ]; + try { + await getSchema(documents, { distinctFieldsAbortThreshold: 4 }); + assert.fail('Analysis did not throw'); + } catch (error) { + assert.strictEqual( + (error as Error).message, + 'Schema analysis aborted: Fields count above 4', + ); + } + }); + + it('does not count the same field in different documents', async function () { + const documents = [ + { + field1: { + field2: { + field3: 'abc', + }, + }, + }, + { + field1: { + field2: { + field3: 'bca', + }, + }, + }, + ]; + try { + await getSchema(documents, { distinctFieldsAbortThreshold: 4 }); + assert.ok('Analysis finished'); + } catch (error) { + assert.fail('Analysis aborted unexpectedly'); + } + }); + }); +}); diff --git a/packages/mongodb-schema/test/field-order.test.ts b/packages/mongodb-schema/test/field-order.test.ts new file mode 100644 index 000000000..538438d9f --- /dev/null +++ b/packages/mongodb-schema/test/field-order.test.ts @@ -0,0 +1,37 @@ +import assert from 'assert'; + +import getSchema from '../src'; + +describe('order of fields', function () { + it('should have _id fields always at top, even with uppercase fields', async function () { + const docs = [ + { + FOO: 1, + _id: 1, + BAR: 1, + zoo: 1, + }, + ]; + const schema = await getSchema(docs); + assert.deepEqual( + schema.fields.map((v) => v.name), + ['_id', 'BAR', 'FOO', 'zoo'], + ); + }); + it('should sort keys in case-insensitive manner', async function () { + const docs = [ + { + cb: 1, + Ca: 1, + cC: 1, + a: 1, + b: 1, + }, + ]; + const schema = await getSchema(docs); + assert.deepEqual( + schema.fields.map((v) => v.name), + ['a', 'b', 'Ca', 'cb', 'cC'], + ); + }); +}); diff --git a/packages/mongodb-schema/test/get-schema-paths.test.ts b/packages/mongodb-schema/test/get-schema-paths.test.ts new file mode 100644 index 000000000..ed536cb95 --- /dev/null +++ b/packages/mongodb-schema/test/get-schema-paths.test.ts @@ -0,0 +1,113 @@ +import assert from 'assert'; +import type { Document } from 'bson'; + +import { getSchemaPaths } from '../src'; + +describe('getSchemaPaths', function () { + let schemaPaths: string[][]; + + describe('with fields with dots in them', function () { + const docs = [ + { + pineapple: { + orange: { + apple: 1, + }, + 'orange.with.dot': { + bah: 2, + }, + }, + }, + ]; + + before(async function () { + schemaPaths = await getSchemaPaths(docs); + }); + + it('returns an array of the fields', function () { + assert.deepEqual(schemaPaths, [ + ['pineapple'], + ['pineapple', 'orange'], + ['pineapple', 'orange', 'apple'], + ['pineapple', 'orange.with.dot'], + ['pineapple', 'orange.with.dot', 'bah'], + ]); + }); + }); + + describe('with multiple documents with different fields', function () { + const docs = [ + { + pineapple: { + orange: { + apple: 1, + }, + }, + }, + { + pineapple: { + orange: 'ok', + }, + }, + { + pineapple: ['test', '123'], + clementine: false, + }, + ]; + + before(async function () { + schemaPaths = await getSchemaPaths(docs); + }); + + it('returns all of the field paths (sorted)', function () { + assert.deepEqual(schemaPaths, [ + ['clementine'], + ['pineapple'], + ['pineapple', 'orange'], + ['pineapple', 'orange', 'apple'], + ]); + }); + }); + + describe('with nested array documents', function () { + const docs = [ + { + orangutan: [ + { + tuatara: 'yes', + lizard: { + snakes: false, + birds: false, + }, + }, + ], + }, + ]; + + before(async function () { + schemaPaths = await getSchemaPaths(docs); + }); + + it('returns all of the field paths (sorted)', function () { + assert.deepEqual(schemaPaths, [ + ['orangutan'], + ['orangutan', 'lizard'], + ['orangutan', 'lizard', 'birds'], + ['orangutan', 'lizard', 'snakes'], + ['orangutan', 'tuatara'], + ]); + }); + }); + + describe('with no documents', function () { + const docs: Document[] = []; + + before(async function () { + schemaPaths = await getSchemaPaths(docs); + }); + + it('returns no paths', function () { + assert.deepEqual(schemaPaths, []); + }); + }); +}); diff --git a/packages/mongodb-schema/test/integration/generate-and-validate.test.ts b/packages/mongodb-schema/test/integration/generate-and-validate.test.ts new file mode 100644 index 000000000..2d71a6cac --- /dev/null +++ b/packages/mongodb-schema/test/integration/generate-and-validate.test.ts @@ -0,0 +1,235 @@ +/* eslint-disable no-console */ +/* eslint-disable mocha/max-top-level-suites */ +import Ajv2020 from 'ajv/dist/2020'; +import assert from 'assert'; +import { Double, Int32, ObjectId, EJSON } from 'bson'; +import { MongoClient, type Db } from 'mongodb'; +import { mochaTestServer } from '@mongodb-js/compass-test-server'; + +import { allValidBSONTypesWithEdgeCasesDoc } from '../all-bson-types-fixture'; +import { analyzeDocuments } from '../../src'; + +const bsonDocuments = [ + { + _id: new ObjectId('67863e82fb817085a6b0ebad'), + title: 'My book', + year: new Int32(1983), + genres: [ + 'crimi', + 'comedy', + { + short: 'scifi', + long: 'science fiction', + }, + ], + number: Double.fromString('Infinity'), + }, + { + _id: new ObjectId('67863eacfb817085a6b0ebae'), + title: 'Other book', + year: new Int32('1999'), + author: { + name: 'Peter Sonder', + rating: new Double(1.3), + }, + }, +]; + +describe('Documents -> Generate schema -> Validate Documents against the schema', function () { + it('Standard JSON Schema with Relaxed EJSON', async function () { + const ajv = new Ajv2020(); + // First we get the JSON schema from BSON + const analyzedDocuments = await analyzeDocuments(bsonDocuments); + const schema = await analyzedDocuments.getStandardJsonSchema(); + const validate = ajv.compile(schema); + for (const doc of bsonDocuments) { + // Then we get EJSON + const relaxedEJSONDoc = EJSON.serialize(doc, { relaxed: true }); + // And validate it agains the JSON Schema + const valid = validate(relaxedEJSONDoc); + if (validate.errors) console.error('Validation failed', validate.errors); + assert.strictEqual(valid, true); + } + }); +}); + +describe('With a MongoDB Cluster', function () { + let client: MongoClient; + let db: Db; + const cluster = mochaTestServer(); + + before(async function () { + // Connect to the mongodb instance. + const connectionString = cluster().connectionString; + client = new MongoClient(connectionString); + await client.connect(); + db = client.db('test'); + }); + + after(async function () { + await client?.close(); + }); + + describe('Documents -> Generate basic schema -> Use schema in validation rule in MongoDB -> Validate documents against the schema', function () { + before(async function () { + // Create the schema validation rule. + const analyzedDocuments = await analyzeDocuments(bsonDocuments); + const schema = await analyzedDocuments.getMongoDBJsonSchema(); + const validationRule = { + $jsonSchema: schema, + }; + + // Create a collection with the schema validation. + await db.createCollection('books', { + validator: validationRule, + }); + }); + + it('allows inserting valid documents', async function () { + await db.collection('books').insertMany(bsonDocuments); + }); + + it('prevents inserting invalid documents', async function () { + const invalidDocs = [ + { + _id: new ObjectId('67863e82fb817085a6b0ebba'), + title: 'Pineapple 1', + year: new Int32(1983), + genres: [ + 'crimi', + 'comedy', + { + short: 'scifi', + long: 'science fiction', + }, + ], + number: 'an invalid string', + }, + { + _id: new ObjectId('67863eacfb817085a6b0ebbb'), + title: 'Pineapple 2', + year: 'year a string', + }, + { + _id: new ObjectId('67863eacfb817085a6b0ebbc'), + title: 123, + year: new Int32('1999'), + }, + { + _id: new ObjectId('67863eacfb817085a6b0ebbc'), + title: 'No year', + }, + ]; + + for (const doc of invalidDocs) { + try { + await db.collection('books').insertOne(doc); + + throw new Error('This should not be reached'); + } catch (e: any) { + const expectedMessage = 'Document failed validation'; + assert.ok( + e.message.includes(expectedMessage), + `Expected error ${String(e.message)} message to include "${expectedMessage}", doc: ${String(doc._id)}`, + ); + } + } + }); + }); + + describe('[All Types] Documents -> Generate basic schema -> Use schema in validation rule in MongoDB -> Validate documents against the schema', function () { + const allTypesCollection = 'allTypes'; + + before(async function () { + await db + .collection(allTypesCollection) + .insertOne(allValidBSONTypesWithEdgeCasesDoc); + const docsFromCollection = await db + .collection(allTypesCollection) + .find({}, { promoteValues: false }) + .toArray(); + + // Create the schema validation rule. + const analyzedDocuments = await analyzeDocuments(docsFromCollection); + const schema = await analyzedDocuments.getMongoDBJsonSchema(); + const validationRule = { + $jsonSchema: schema, + }; + // Update the collection with the schema validation. + await db.command({ + collMod: allTypesCollection, + validator: validationRule, + }); + }); + + it('allows inserting valid documents (does not error)', async function () { + const docs = [ + { + ...allValidBSONTypesWithEdgeCasesDoc, + _id: new ObjectId(), + }, + { + ...allValidBSONTypesWithEdgeCasesDoc, + _id: new ObjectId(), + }, + ]; + + try { + await db.collection(allTypesCollection).insertMany(docs); + } catch (err) { + console.error( + 'Error inserting documents', + EJSON.stringify(err, undefined, 2), + ); + throw err; + } + }); + + it('prevents inserting invalid documents', async function () { + const invalidDocs = [ + { + _id: new ObjectId('67863e82fb817085a6b0ebba'), + title: 'Pineapple 1', + year: new Int32(1983), + genres: [ + 'crimi', + 'comedy', + { + short: 'scifi', + long: 'science fiction', + }, + ], + number: 'an invalid string', + }, + { + _id: new ObjectId('67863eacfb817085a6b0ebbb'), + title: 'Pineapple 2', + year: 'year a string', + }, + { + _id: new ObjectId('67863eacfb817085a6b0ebbc'), + title: 123, + year: new Int32('1999'), + }, + { + _id: new ObjectId('67863eacfb817085a6b0ebbc'), + title: 'No year', + }, + ]; + + for (const doc of invalidDocs) { + try { + await db.collection(allTypesCollection).insertOne(doc); + + throw new Error('This should not be reached'); + } catch (e: any) { + const expectedMessage = 'Document failed validation'; + assert.ok( + e.message.includes(expectedMessage), + `Expected error ${String(e.message)} message to include "${expectedMessage}", doc: ${String(doc._id)}`, + ); + } + } + }); + }); +}); diff --git a/packages/mongodb-schema/test/mixed-type-evolving-schema.test.ts b/packages/mongodb-schema/test/mixed-type-evolving-schema.test.ts new file mode 100644 index 000000000..2604d5b3c --- /dev/null +++ b/packages/mongodb-schema/test/mixed-type-evolving-schema.test.ts @@ -0,0 +1,81 @@ +import assert from 'assert'; +import BSON from 'bson'; + +import type { SchemaField } from '../src/schema-analyzer'; +import getSchema from '../src'; + +describe('evolving schema', function () { + // The hardest case and really why this module exists at all: proper + // handling for polymorphic schemas. Consider the following scenario: + // + // 1. started out with schema in `only basic fields`. + // 2. then read a blog post about how awesome; embedded documents are. + // 3. then realized what a pain embedded documents are. + let applePushToken: SchemaField | undefined; + const docs = [ + { + _id: new BSON.ObjectId('55582407aafa8fbbc57196e2'), + name: 'Brett Flowers', + email: { + _id: 'gohu@pum.io', + is_verified: false, + }, + twitter: { + username: '@lekbisova', + }, + stats: { + friends: 7584, + }, + push_token: { + android: undefined, + apple: + '4e2e068cd281cfe924ff3174dfe363bd3108a5852ca5197f37c40c1bca6e1a4c', + }, + last_address: { + latitude: null, + longitude: null, + }, + created_at: new Date(), + }, + { + _id: new BSON.ObjectId('55581e0a9bf712d0c2b48d71'), + email: 'tupjud@weigehib.gov', + is_verified: false, + twitter_username: '@zaetisi', + name: 'Hunter Maxwell', + stats_friends: 2163, + apple_push_token: + 'd4b4e7f3361cec05fae848575d7e6e1da2f0dccdf8ccc86a8ff2124d8b0542f6', + android_push_token: undefined, + last_address_latitude: null, + last_address_longitude: null, + created_at: new Date(), + }, + ]; + + before(async function () { + const users = await getSchema(docs); + applePushToken = users.fields.find((v) => v.name === 'apple_push_token'); + }); + it('should have the `apple_push_token` field', function () { + assert.ok(applePushToken); + }); + it('should have seen `apple_push_token` 1 time', function () { + assert.equal(applePushToken?.count, 1); + }); + it('should have seen `apple_push_token` in 50% of documents', function () { + assert.equal(applePushToken?.probability, 0.5); + }); + it('should have seen `apple_push_token` 1 time as a string', function () { + assert.equal( + applePushToken?.types.find((v) => v.name === 'String')?.count, + 1, + ); + }); + it('should have seen 1 unique string value for `apple_push_token`', function () { + assert.equal( + applePushToken?.types.find((v) => v.name === 'String')?.unique, + 1, + ); + }); +}); diff --git a/packages/mongodb-schema/test/mixed-type-nested.test.ts b/packages/mongodb-schema/test/mixed-type-nested.test.ts new file mode 100644 index 000000000..a6227a92b --- /dev/null +++ b/packages/mongodb-schema/test/mixed-type-nested.test.ts @@ -0,0 +1,101 @@ +import assert from 'assert'; + +import getSchema from '../src'; +import type { + DocumentSchemaType, + Schema, + SchemaField, +} from '../src/schema-analyzer'; + +describe('mixed types nested', function () { + const docs = [ + { + _id: 1, + address: { + valid: 0, + }, + }, + { + _id: 2, + address: { + valid: false, + }, + }, + { + _id: 3, + address: { + valid: 'None', + }, + }, + { + _id: 4, + address: {}, + }, + { + _id: 5, + address: { + valid: true, + toString: { value: false }, + }, + }, + ] as const; + + let schema: Schema; + let valid: SchemaField | undefined; + + before(async function () { + schema = await getSchema(docs); + if (!schema.fields.find((v) => v.name === '_id')) { + throw new Error('Did not pick up `_id` field'); + } + + valid = ( + schema.fields + .find((v) => v.name === 'address') + ?.types.find((x) => x.name === 'Document') as DocumentSchemaType + )?.fields.find((v) => v.name === 'valid'); + if (!valid) { + throw new Error('Did not pick up `address.valid` field'); + } + if (!valid.types.find((v) => v.name === 'Undefined')) { + throw new Error('Missing Undefined type on `address.valid`'); + } + }); + + it('should see the `address` field is always present', function () { + assert.equal( + schema.fields.find((v) => v.name === 'address')?.probability, + 1, + ); + }); + it('should see the `valid` field in 80% of documents', function () { + assert.equal(valid?.probability, 0.8); + }); + it('should see there are 4 possible types for `valid`', function () { + assert.equal(valid?.types.length, 4); + }); + it('should see `Number` was used in 20% of documents', function () { + assert.equal( + valid?.types.find((v) => v.name === 'Number')?.probability, + 0.2, + ); + }); + it('should see `Boolean` was used in 40% of documents', function () { + assert.equal( + valid?.types.find((v) => v.name === 'Boolean')?.probability, + 0.4, + ); + }); + it('should see `Undefined` was used in 20% of documents', function () { + assert.equal( + valid?.types.find((v) => v.name === 'Undefined')?.probability, + 0.2, + ); + }); + it('should see `String` was used in 20% of documents', function () { + assert.equal( + valid?.types.find((v) => v.name === 'String')?.probability, + 0.2, + ); + }); +}); diff --git a/packages/mongodb-schema/test/mixed-type-order.test.ts b/packages/mongodb-schema/test/mixed-type-order.test.ts new file mode 100644 index 000000000..c52198dd4 --- /dev/null +++ b/packages/mongodb-schema/test/mixed-type-order.test.ts @@ -0,0 +1,47 @@ +import assert from 'assert'; + +import type { SchemaField } from '../src/schema-analyzer'; +import getSchema from '../src'; + +describe('mixed type order', function () { + const docs = [ + { + _id: 2, + }, + { + _id: 3, + registered: '1', + }, + { + _id: 4, + registered: '1', + }, + { + _id: 1, + registered: 1, + }, + ]; + + let registered: SchemaField | undefined; + before(async function () { + const schema = await getSchema(docs); + registered = schema.fields.find((v) => v.name === 'registered'); + + if (!registered) { + throw new Error('Did not pick up `registered` field'); + } + if (!registered.types.find((v) => v.name === 'Undefined')) { + throw new Error('Missing Undefined type on `registered`'); + } + }); + it('should have 3 types for `registered`', function () { + assert.equal(registered?.types.length, 3); + }); + it('should return the order of types as ["String", "Number", "Undefined"]', function (done) { + assert.deepEqual( + registered?.types.map((v) => v.name), + ['String', 'Number', 'Undefined'], + ); + done(); + }); +}); diff --git a/packages/mongodb-schema/test/mixed-type-probability.test.ts b/packages/mongodb-schema/test/mixed-type-probability.test.ts new file mode 100644 index 000000000..737d43a5b --- /dev/null +++ b/packages/mongodb-schema/test/mixed-type-probability.test.ts @@ -0,0 +1,74 @@ +import assert from 'assert'; + +import getSchema from '../src'; +import type { Schema, SchemaField } from '../src/schema-analyzer'; + +describe('mixed type probability', function () { + const docs = [ + { + _id: 1, + registered: 1, + }, + { + _id: 2, + registered: '1', + }, + { + _id: 3, + registered: true, + }, + { + _id: 4, + assigned: true, + }, + ]; + + let schema: Schema; + let registered: SchemaField | undefined; + before(async function () { + schema = await getSchema(docs); + registered = schema.fields.find((v) => v.name === 'registered'); + + if (!registered) { + throw new Error('Did not pick up `registered` field'); + } + if (!registered.types.find((v) => v.name === 'Undefined')) { + throw new Error('Missing Undefined type on `registered`'); + } + }); + it('should have 4 types for `registered`', function () { + assert.equal(registered?.types.length, 4); + }); + it('should have a probability of 25% for `registered` to be a boolean', function () { + assert.equal( + registered?.types.find((v) => v.name === 'Boolean')?.probability, + 1 / 4, + ); + }); + it('should have a probability of 25% for `registered` to be a number', function () { + assert.equal( + registered?.types.find((v) => v.name === 'Number')?.probability, + 1 / 4, + ); + }); + it('should have a probability of 25% for `registered` to be a string', function () { + assert.equal( + registered?.types.find((v) => v.name === 'String')?.probability, + 1 / 4, + ); + }); + it('should have a probability of 25% for `registered` to be undefined', function () { + assert.equal( + registered?.types.find((v) => v.name === 'Undefined')?.probability, + 1 / 4, + ); + }); + it('should compensate for missed Undefined values', function () { + const assigned = schema.fields.find((v) => v.name === 'assigned'); + assert.equal(assigned?.probability, 0.25); + assert.equal( + assigned?.types.find((v) => v.name === 'Boolean')?.probability, + 0.25, + ); + }); +}); diff --git a/packages/mongodb-schema/test/nested-document-path.test.ts b/packages/mongodb-schema/test/nested-document-path.test.ts new file mode 100644 index 000000000..04ef4fba3 --- /dev/null +++ b/packages/mongodb-schema/test/nested-document-path.test.ts @@ -0,0 +1,49 @@ +import assert from 'assert'; + +import getSchema from '../src'; +import type { Schema, DocumentSchemaType } from '../src/schema-analyzer'; + +describe('nested document path', function () { + let schema: Schema; + const docs = [ + { + foo: { + bar: { + baz: 1, + }, + 'bar.with.dot': { + bah: 2, + }, + }, + }, + ]; + + before(async function () { + schema = await getSchema(docs); + }); + + it('should assemble the path correctly fields with dots in their names', function () { + const foo = schema.fields.find((v) => v.name === 'foo'); + const bar = ( + foo?.types.find((v) => v.name === 'Document') as DocumentSchemaType + )?.fields.find((v) => v.name === 'bar'); + const baz = ( + bar?.types.find((v) => v.name === 'Document') as DocumentSchemaType + )?.fields.find((v) => v.name === 'baz'); + assert.ok(foo); + assert.ok(bar); + assert.ok(baz); + assert.deepEqual(baz.path, ['foo', 'bar', 'baz']); + + const barWithDot = ( + foo.types.find((v) => v.name === 'Document') as DocumentSchemaType + )?.fields.find((v) => v.name === 'bar.with.dot'); + assert.ok(barWithDot); + const bah = ( + barWithDot.types.find((v) => v.name === 'Document') as DocumentSchemaType + )?.fields.find((v) => v.name === 'bah'); + assert.ok(bah); + assert.deepEqual(barWithDot.path, ['foo', 'bar.with.dot']); + assert.deepEqual(bah.path, ['foo', 'bar.with.dot', 'bah']); + }); +}); diff --git a/packages/mongodb-schema/test/no-node.test.ts b/packages/mongodb-schema/test/no-node.test.ts new file mode 100644 index 000000000..57486fa40 --- /dev/null +++ b/packages/mongodb-schema/test/no-node.test.ts @@ -0,0 +1,96 @@ +import assert from 'assert'; +import vm from 'vm'; +import fs from 'fs'; +import path from 'path'; + +import type * as mongodbSchema from '..'; + +function createMockModuleSystem() { + // `setTimeout` is available in every JS environment (browsers included), but + // is not part of the ECMAScript spec and so is absent from a bare vm context. + const context = vm.createContext( + Object.assign(Object.create(null), { setTimeout }), + ); + class Module { + exports = {}; + } + const modules = new Map(); + // Tiny (incomplete) CommonJS module system mock + function makeRequire(basename: string) { + return function require(identifier: string): any { + if ( + !identifier.startsWith('./') && + !identifier.startsWith('../') && + !path.isAbsolute(identifier) + ) { + let current = path.dirname(basename); + let previous: string; + do { + const nodeModulesEntry = path.resolve( + current, + 'node_modules', + identifier, + ); + previous = current; + current = path.dirname(current); + if (fs.existsSync(nodeModulesEntry)) { + return require(nodeModulesEntry); + } + } while (previous !== current); + throw new Error( + `mock require() does not support Node.js built-ins (${identifier})`, + ); + } + let file = path.resolve(path.dirname(basename), identifier); + if (!fs.existsSync(file) && fs.existsSync(`${file}.js`)) { + file = `${file}.js`; + } else if (fs.statSync(file).isDirectory()) { + if (fs.existsSync(`${file}/package.json`)) { + const pjson = JSON.parse( + fs.readFileSync(`${file}/package.json`, 'utf8'), + ); + file = path.resolve(file, pjson.main || 'index.js'); + } else if (fs.existsSync(`${file}/index.js`)) { + file = path.resolve(file, 'index.js'); + } + } + const existing = modules.get(file); + if (existing) { + return existing.exports; + } + const module = new Module(); + const source = fs.readFileSync(file); + vm.runInContext( + `(function(require, module, exports, __filename, __dirname) {\n${source.toString('utf8')}\n})`, + context, + )(makeRequire(file), module, module.exports, file, path.dirname(file)); + modules.set(file, module); + return module.exports; + }; + } + return makeRequire; +} + +describe('getSchema should work in plain JS environment without Node.js or browser dependencies', function () { + const docs = [ + { foo: 'bar' }, + { country: 'Croatia' }, + { country: 'Croatia' }, + { country: 'England' }, + ]; + + it('Check if return value is a promise', async function () { + const makeRequire = createMockModuleSystem(); + + const { parseSchema } = makeRequire(__filename)( + '../dist/index.js', + ) as typeof mongodbSchema; + + const result = await parseSchema(docs); + assert.strictEqual(result.count, 4); + assert.strictEqual( + result.fields.map((f) => f.name).join(','), + 'country,foo', + ); + }); +}); diff --git a/packages/mongodb-schema/test/promise.test.ts b/packages/mongodb-schema/test/promise.test.ts new file mode 100644 index 000000000..81e09cf21 --- /dev/null +++ b/packages/mongodb-schema/test/promise.test.ts @@ -0,0 +1,56 @@ +import assert from 'assert'; + +import type { Schema } from '../src/schema-analyzer'; +import getSchema from '../src'; + +describe('getSchema should return promise', function () { + const docs = [ + { foo: 'bar' }, + { country: 'Croatia' }, + { country: 'Croatia' }, + { country: 'England' }, + ]; + + it('Check if return value is a promise', function () { + const result = getSchema(docs); + assert.strictEqual(result instanceof Promise, true); + }); + + it('Check that promise returns expected schema', async function () { + const result = await getSchema(docs); + const fieldNames = result.fields.map((v) => v.name); + assert.deepStrictEqual(fieldNames, ['country', 'foo']); + }); + + describe('Check promise success', function () { + let response: Schema; + + before(async function () { + response = await getSchema(docs); + }); + + it('Using promise should succeed', function () { + assert.equal(response.count, 4); + assert.equal(response.fields.length, 2); + }); + }); + + describe('Check failure', function () { + let errMsg: string | undefined; + + before(async function () { + try { + await getSchema({ foo: 'bar' } as any); + } catch (err: any) { + errMsg = err?.message; + } + }); + + it('Using promise should give an error message', function () { + assert.strictEqual( + errMsg, + 'Unknown input type for `docs`. Must be an array, stream or MongoDB Cursor.', + ); + }); + }); +}); diff --git a/packages/mongodb-schema/test/regression-strings-have-same-probability.test.ts b/packages/mongodb-schema/test/regression-strings-have-same-probability.test.ts new file mode 100644 index 000000000..8e644f04d --- /dev/null +++ b/packages/mongodb-schema/test/regression-strings-have-same-probability.test.ts @@ -0,0 +1,37 @@ +import assert from 'assert'; + +import getSchema from '../src'; +import type { Schema, PrimitiveSchemaType } from '../src/schema-analyzer'; + +describe('regression', function () { + describe('strings have same probability', function () { + const docs = [ + { + _id: 1, + value: 'DUPE', + }, + { + _id: 2, + value: 'DUPE', + }, + { + _id: 3, + value: 'DUPE', + }, + ]; + + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should not dedupe values but return all 3 of them', function () { + const types = schema.fields.find((v) => v.name === 'value')?.types; + assert.equal( + (types?.find((v) => v.name === 'String') as PrimitiveSchemaType)?.values + .length, + 3, + ); + }); + }); +}); diff --git a/packages/mongodb-schema/test/schema-accessor.test.ts b/packages/mongodb-schema/test/schema-accessor.test.ts new file mode 100644 index 000000000..212fa3177 --- /dev/null +++ b/packages/mongodb-schema/test/schema-accessor.test.ts @@ -0,0 +1,61 @@ +import { type InternalSchema } from '../src'; +import sinon from 'sinon'; +import assert from 'assert'; +import type { InternalConverters } from '../src/schema-accessor'; +import { InternalSchemaBasedAccessor } from '../src/schema-accessor'; + +describe('analyzeDocuments', function () { + const internalSchema: InternalSchema = { + count: 1, + fields: [], + }; + let sandbox: sinon.SinonSandbox; + let converters: InternalConverters; + + before(function () { + sandbox = sinon.createSandbox(); + }); + + beforeEach(function () { + converters = { + internalToStandard: sandbox.stub(), + internalToMongoDB: sandbox.stub(), + internalToExpanded: sandbox.stub(), + }; + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('Converts lazily', async function () { + const schema = new InternalSchemaBasedAccessor(internalSchema, converters); + assert.strictEqual( + (converters.internalToStandard as sinon.SinonStub).called, + false, + ); + await schema.getStandardJsonSchema(); + assert.strictEqual( + (converters.internalToStandard as sinon.SinonStub).calledOnce, + true, + ); + }); + + it('Only converts the same format once', async function () { + const schema = new InternalSchemaBasedAccessor(internalSchema, converters); + assert.strictEqual( + (converters.internalToExpanded as sinon.SinonStub).called, + false, + ); + (converters.internalToExpanded as sinon.SinonStub).returns({ + abc: 'string', + }); + await schema.getExpandedJSONSchema(); + await schema.getExpandedJSONSchema(); + await schema.getExpandedJSONSchema(); + assert.strictEqual( + (converters.internalToExpanded as sinon.SinonStub).calledOnce, + true, + ); + }); +}); diff --git a/packages/mongodb-schema/test/semantic-types.test.ts b/packages/mongodb-schema/test/semantic-types.test.ts new file mode 100644 index 000000000..7b8004ac0 --- /dev/null +++ b/packages/mongodb-schema/test/semantic-types.test.ts @@ -0,0 +1,212 @@ +import assert from 'assert'; +import BSON from 'bson'; + +import getSchema from '../src'; +import type { Schema, PrimitiveSchemaType } from '../src/schema-analyzer'; + +describe('options', function () { + const docs = [ + { + _id: new BSON.ObjectId('55581e0a9bf712d0c2b48d71'), + email: 'tick@duck.org', + shape: { + type: 'Point', + coordinates: [20.0, 30.0], + }, + is_verified: false, + }, + { + _id: new BSON.ObjectId('55581e0a9bf712d0c2b48d72'), + email: 'trick@duck.org', + shape: { + type: 'LineString', + coordinates: [ + [20.0, 30.0], + [30.0, 40.0], + ], + }, + is_verified: false, + }, + { + _id: new BSON.ObjectId('55581e0a9bf712d0c2b48d73'), + email: 'track@duck.org', + shape: { + type: 'Polygon', + coordinates: [ + [ + [20.0, 30.0], + [30.0, 40.0], + [15.0, 50.0], + [20.0, 30.0], + ], + ], + }, + is_verified: true, + }, + ]; + + let schema: Schema; + context('when using default options', function () { + beforeEach(async function () { + schema = await getSchema(docs); + }); + + it('stores values by default', function () { + assert.equal( + (schema.fields[0].types[0] as PrimitiveSchemaType).values.length, + 3, + ); + }); + + it('does not use semantic type detection', function () { + assert.equal( + schema.fields.find((v) => v.name === 'email')?.types[0].name, + 'String', + ); + assert.equal( + (schema.fields.find((v) => v.name === 'email')?.types[0] as any) + .bsonType, + 'String', + ); + assert.equal( + schema.fields.find((v) => v.name === 'shape')?.types[0].name, + 'Document', + ); + assert.equal( + (schema.fields.find((v) => v.name === 'shape')?.types[0] as any) + .bsonType, + 'Document', + ); + }); + }); + + context('when `storeValues` is false', function () { + beforeEach(async function () { + schema = await getSchema(docs, { storeValues: false }); + }); + it('does not store values when `storeValues` is false', function () { + assert.ok(!(schema.fields[0].types[0] as PrimitiveSchemaType).values); + }); + }); + + context('when `semanticTypes` is true', function () { + beforeEach(async function () { + schema = await getSchema(docs, { semanticTypes: true }); + }); + it('calls semantic type detection', function () { + assert.equal( + schema.fields.find((v) => v.name === 'email')?.types[0].name, + 'Email', + ); + assert.equal( + (schema.fields.find((v) => v.name === 'email')?.types[0] as any) + .bsonType, + 'String', + ); + }); + }); + context('when `semanticTypes` is an object', function () { + context('and all values are boolean', function () { + beforeEach(async function () { + schema = await getSchema(docs, { semanticTypes: { email: true } }); + }); + it('only uses the enabled type detectors', function () { + assert.equal( + schema.fields.find((v) => v.name === 'email')?.types[0].name, + 'Email', + ); + assert.equal( + (schema.fields.find((v) => v.name === 'email')?.types[0] as any) + .bsonType, + 'String', + ); + }); + }); + context('and values are mixed upper/lower case', function () { + beforeEach(async function () { + schema = await getSchema(docs, { semanticTypes: { eMaIl: true } }); + }); + it('uses the enabled type detectors', function () { + assert.equal( + schema.fields.find((v) => v.name === 'email')?.types[0].name, + 'Email', + ); + assert.equal( + (schema.fields.find((v) => v.name === 'email')?.types[0] as any) + .bsonType, + 'String', + ); + }); + }); + + context('and all values are custom detector functions', function () { + beforeEach(async function () { + schema = await getSchema(docs, { + semanticTypes: { + Verification: function (value, key) { + return !!key?.pop?.()?.match(/verified/); + }, + }, + }); + }); + it('uses the custom type detectors', function () { + assert.equal( + schema.fields.find((v) => v.name === 'is_verified')?.types[0].name, + 'Verification', + ); + assert.equal( + (schema.fields.find((v) => v.name === 'is_verified')?.types[0] as any) + .bsonType, + 'Boolean', + ); + assert.equal( + schema.fields.find((v) => v.name === 'email')?.types[0].name, + 'String', + ); + assert.equal( + (schema.fields.find((v) => v.name === 'email')?.types[0] as any) + .bsonType, + 'String', + ); + }); + }); + + context( + 'and values are mixed booleans and custom detector functions', + function () { + beforeEach(async function () { + schema = await getSchema(docs, { + semanticTypes: { + email: true, + Verification: function (value, key) { + return !!key?.pop?.()?.match(/verified/); + }, + }, + }); + }); + it('uses the enabled and custom type detectors', function () { + assert.equal( + schema.fields.find((v) => v.name === 'is_verified')?.types[0].name, + 'Verification', + ); + assert.equal( + ( + schema.fields.find((v) => v.name === 'is_verified') + ?.types[0] as any + ).bsonType, + 'Boolean', + ); + assert.equal( + schema.fields.find((v) => v.name === 'email')?.types[0].name, + 'Email', + ); + assert.equal( + (schema.fields.find((v) => v.name === 'email')?.types[0] as any) + .bsonType, + 'String', + ); + }); + }, + ); + }); +}); diff --git a/packages/mongodb-schema/test/simplified-schema.test.ts b/packages/mongodb-schema/test/simplified-schema.test.ts new file mode 100644 index 000000000..2d9e0465a --- /dev/null +++ b/packages/mongodb-schema/test/simplified-schema.test.ts @@ -0,0 +1,330 @@ +import assert from 'assert'; + +import { getSimplifiedSchema } from '../src'; +import type { SimplifiedSchema } from '../src/schema-analyzer'; +import { allBSONTypesDoc } from './all-bson-types-fixture'; + +const docsFixture = [ + { + foo: 1, + bar: 25, + }, + { + foo: 2, + bar: 'test', + baz: true, + }, + { + foo: 3, + bar: 'another test', + }, + allBSONTypesDoc, +]; + +const expected = { + _id: { + types: [ + { + bsonType: 'ObjectId', + }, + ], + }, + double: { + types: [ + { + bsonType: 'Double', + }, + ], + }, + string: { + types: [ + { + bsonType: 'String', + }, + ], + }, + object: { + types: [ + { + bsonType: 'Document', + fields: { + key: { + types: [ + { + bsonType: 'String', + }, + ], + }, + }, + }, + ], + }, + array: { + types: [ + { + bsonType: 'Array', + types: [ + { + bsonType: 'Number', + }, + ], + }, + ], + }, + binData: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + objectId: { + types: [ + { + bsonType: 'ObjectId', + }, + ], + }, + boolean: { + types: [ + { + bsonType: 'Boolean', + }, + ], + }, + date: { + types: [ + { + bsonType: 'Date', + }, + ], + }, + null: { + types: [ + { + bsonType: 'Null', + }, + ], + }, + regex: { + types: [ + { + bsonType: 'BSONRegExp', + }, + ], + }, + javascript: { + types: [ + { + bsonType: 'Code', + }, + ], + }, + symbol: { + types: [ + { + bsonType: 'BSONSymbol', + }, + ], + }, + javascriptWithScope: { + types: [ + { + bsonType: 'CodeWScope', + }, + ], + }, + int: { + types: [ + { + bsonType: 'Int32', + }, + ], + }, + timestamp: { + types: [ + { + bsonType: 'Timestamp', + }, + ], + }, + long: { + types: [ + { + bsonType: 'Long', + }, + ], + }, + decimal: { + types: [ + { + bsonType: 'Decimal128', + }, + ], + }, + minKey: { + types: [ + { + bsonType: 'MinKey', + }, + ], + }, + maxKey: { + types: [ + { + bsonType: 'MaxKey', + }, + ], + }, + + binaries: { + types: [ + { + bsonType: 'Document', + fields: { + generic: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + functionData: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + binaryOld: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + uuidOld: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + uuid: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + md5: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + encrypted: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + compressedTimeSeries: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + custom: { + types: [ + { + bsonType: 'Binary', + }, + ], + }, + }, + }, + ], + }, + dbRef: { + types: [ + { + bsonType: 'DBRef', + }, + ], + }, + foo: { + types: [ + { + bsonType: 'Number', + }, + ], + }, + bar: { + types: [ + { + bsonType: 'String', + }, + { + bsonType: 'Number', + }, + ], + }, + baz: { + types: [ + { + bsonType: 'Boolean', + }, + ], + }, +}; + +describe('simplified schema', function () { + let schema: SimplifiedSchema; + + before(async function () { + schema = await getSimplifiedSchema(docsFixture); + }); + + it('should assemble a simplified schema', function () { + assert.deepEqual(schema, expected); + }); + + it('contains all of the types', function () { + const fieldTypes = [ + 'Array', + 'Binary', + 'Boolean', + 'Code', + 'Date', + 'Decimal128', + 'Double', + 'Int32', + 'Long', + 'MaxKey', + 'MinKey', + 'Null', + 'Document', + 'ObjectId', + 'BSONRegExp', + 'String', + 'BSONSymbol', + 'Timestamp', + ]; + + for (const fieldType of fieldTypes) { + assert.equal( + !!Object.values(schema).find((schemaType) => + schemaType.types.find( + (schemaType) => schemaType.bsonType === fieldType, + ), + ), + true, + `Cannot find type "${fieldType}" in schemaField types: ${JSON.stringify(schema.fields, null, 2)}`, + ); + } + }); +}); diff --git a/packages/mongodb-schema/test/stats.test.ts b/packages/mongodb-schema/test/stats.test.ts new file mode 100644 index 000000000..8016ec60b --- /dev/null +++ b/packages/mongodb-schema/test/stats.test.ts @@ -0,0 +1,178 @@ +import assert from 'assert'; + +import getSchema from '../src'; +import stats from '../src/stats'; +import type { Schema } from '../src/schema-analyzer'; + +describe('schema statistics', function () { + describe('empty doc', function () { + const docs = [{}]; + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should have the correct schema width', function () { + assert.equal(stats.width(schema), 0); + }); + it('should have the correct schema depth', function () { + assert.equal(stats.depth(schema), 0); + }); + }); + describe('doc with one key', function () { + const docs = [ + { + foo: 'bar', + }, + ]; + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should have the correct schema width', function () { + assert.equal(stats.width(schema), 1); + }); + it('should have the correct schema depth', function () { + assert.equal(stats.depth(schema), 1); + }); + }); + describe('example 1', function () { + const docs = [ + { + one: [ + 'foo', + 'bar', + { + two: { + three: 3, + }, + }, + 'baz', + ], + foo: 'bar', + }, + ]; + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should have the correct schema width', function () { + assert.equal(stats.width(schema), 4); + }); + it('should have the correct schema depth', function () { + assert.equal(stats.depth(schema), 3); + }); + }); + describe('example 2', function () { + const docs = [ + { + x: [1, 2, 3], + }, + { + x: 'foo', + }, + { + x: { + b: 1, + }, + }, + { + x: ['bar', null, false], + }, + { + x: [ + { + c: 1, + d: 1, + }, + { + c: 2, + }, + ], + }, + { + e: 1, + }, + ]; + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should have the correct schema width', function () { + assert.equal(stats.width(schema), 5); + }); + it('should have the correct schema depth', function () { + assert.equal(stats.depth(schema), 2); + }); + }); + describe('example 3', function () { + const docs = [ + { + a: 1, + b: false, + one: { + c: null, + two: { + three: { + four: 4, + e: 'deepest nesting level', + }, + }, + }, + f: { + g: 'not the deepest level', + }, + }, + ]; + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should have the correct schema width', function () { + assert.equal(stats.width(schema), 10); + }); + it('should have the correct schema depth', function () { + assert.equal(stats.depth(schema), 4); + }); + }); + describe('example 4', function () { + const docs = [ + { + a: { + b: [ + { + c: { + d: [ + { + e: { + f: [ + { + g: [1, 2, 3], + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ]; + let schema: Schema; + before(async function () { + schema = await getSchema(docs); + }); + + it('should have the correct schema width', function () { + assert.equal(stats.width(schema), 7); + }); + it('should have the correct schema depth', function () { + assert.equal(stats.depth(schema), 7); + }); + }); +}); diff --git a/packages/mongodb-schema/test/wrapper.test.ts b/packages/mongodb-schema/test/wrapper.test.ts new file mode 100644 index 000000000..832cb5888 --- /dev/null +++ b/packages/mongodb-schema/test/wrapper.test.ts @@ -0,0 +1,36 @@ +import assert from 'assert'; + +import getSchema from '../src'; + +describe('Convenience Wrapper', function () { + it('should be available as a function', function () { + assert.ok(getSchema); + assert.equal(typeof getSchema, 'function'); + }); + + it('should accept documents as an array and return a schema', async function () { + const res = await getSchema([ + { + a: 1, + }, + { + a: 2, + b: false, + }, + ]); + assert.ok(res); + assert.equal(typeof res, 'object'); + assert.ok(!!res.count); + assert.ok(!!res.fields); + assert.equal(res.count, 2); + }); + + it('should return an error when no documents are provided', async function () { + try { + await getSchema('' as any); + assert(false); // Should error. + } catch (err) { + assert.ok(err); + } + }); +}); diff --git a/packages/mongodb-schema/tsconfig-lint.json b/packages/mongodb-schema/tsconfig-lint.json new file mode 100644 index 000000000..6bdef84f3 --- /dev/null +++ b/packages/mongodb-schema/tsconfig-lint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/mongodb-schema/tsconfig.json b/packages/mongodb-schema/tsconfig.json new file mode 100644 index 000000000..d9e86b4ab --- /dev/null +++ b/packages/mongodb-schema/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@mongodb-js/tsconfig-devtools/tsconfig.common.json", + "exclude": ["src/**/*.spec.*", "src/**/*.test.*"] +} diff --git a/packages/mongodb-ts-autocomplete/package.json b/packages/mongodb-ts-autocomplete/package.json index 1ff8d04e5..6e0e4f332 100644 --- a/packages/mongodb-ts-autocomplete/package.json +++ b/packages/mongodb-ts-autocomplete/package.json @@ -53,11 +53,11 @@ "extract-types": "ts-node scripts/extract-types.ts" }, "dependencies": { + "@mongodb-js/mongodb-schema": "^0.1.0", "@mongodb-js/ts-autocomplete": "^0.5.13", "@mongosh/shell-api": "^5.1.6", "debug": "^4.4.0", "lodash": "^4.17.21", - "mongodb-schema": "^12.6.2", "node-cache": "^5.1.2", "typescript": "^5.9.3" }, diff --git a/packages/mongodb-ts-autocomplete/src/autocompleter.spec.ts b/packages/mongodb-ts-autocomplete/src/autocompleter.spec.ts index 6a5f9fca1..cbaeb72c7 100644 --- a/packages/mongodb-ts-autocomplete/src/autocompleter.spec.ts +++ b/packages/mongodb-ts-autocomplete/src/autocompleter.spec.ts @@ -1,7 +1,7 @@ import * as ts from 'typescript'; import { MongoDBAutocompleter } from './index'; import type { AutocompletionContext } from './autocompletion-context'; -import { analyzeDocuments } from 'mongodb-schema'; +import { analyzeDocuments } from '@mongodb-js/mongodb-schema'; import { expect } from 'chai'; import { relativeNodePath } from '@mongodb-js/ts-autocomplete'; diff --git a/packages/mongodb-ts-autocomplete/src/autocompleter.ts b/packages/mongodb-ts-autocomplete/src/autocompleter.ts index bd6e643b8..a695bc521 100644 --- a/packages/mongodb-ts-autocomplete/src/autocompleter.ts +++ b/packages/mongodb-ts-autocomplete/src/autocompleter.ts @@ -7,8 +7,8 @@ import autocompleteTypes from './types/autocomplete-types'; import { api as ShellApiText } from '@mongosh/shell-api/api'; import { replaceImports } from './utils'; -import type { JSONSchema } from 'mongodb-schema'; -import { toTypescriptTypeDefinition } from 'mongodb-schema'; +import type { JSONSchema } from '@mongodb-js/mongodb-schema'; +import { toTypescriptTypeDefinition } from '@mongodb-js/mongodb-schema'; import { inferCollectionNameFromFunctionCall, diff --git a/packages/mongodb-ts-autocomplete/src/autocompletion-context.ts b/packages/mongodb-ts-autocomplete/src/autocompletion-context.ts index 6fbbf11ce..204f8578a 100644 --- a/packages/mongodb-ts-autocomplete/src/autocompletion-context.ts +++ b/packages/mongodb-ts-autocomplete/src/autocompletion-context.ts @@ -1,6 +1,6 @@ import NodeCache from 'node-cache'; -import type { JSONSchema } from 'mongodb-schema'; +import type { JSONSchema } from '@mongodb-js/mongodb-schema'; type CacheOptions = { databaseCollectionsTTL: number; diff --git a/packages/mql-typescript/package.json b/packages/mql-typescript/package.json index 0d9dbb633..007346425 100644 --- a/packages/mql-typescript/package.json +++ b/packages/mql-typescript/package.json @@ -72,6 +72,7 @@ "devDependencies": { "@mongodb-js/eslint-config-devtools": "^0.11.8", "@mongodb-js/prettier-config-devtools": "^1.0.4", + "@mongodb-js/mongodb-schema": "^0.1.0", "@mongodb-js/shell-bson-parser": "^1.5.14", "@mongodb-js/tsconfig-devtools": "^1.1.3", "@types/chai": "^4.2.21", @@ -84,7 +85,6 @@ "js-yaml": "^4.1.1", "jsdom": "^24.1.3", "json-schema-to-zod": "^2.6.0", - "mongodb-schema": "^12.6.2", "prettier": "^3.8.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", diff --git a/packages/mql-typescript/src/driverSchema/docs-crawler.ts b/packages/mql-typescript/src/driverSchema/docs-crawler.ts index 2854321a4..d2ef86e18 100644 --- a/packages/mql-typescript/src/driverSchema/docs-crawler.ts +++ b/packages/mql-typescript/src/driverSchema/docs-crawler.ts @@ -1,6 +1,6 @@ import { removeNewlines, removeTrailingComments } from '../utils'; -import { getSimplifiedSchema } from 'mongodb-schema'; -import type { SimplifiedSchema } from 'mongodb-schema'; +import { getSimplifiedSchema } from '@mongodb-js/mongodb-schema'; +import type { SimplifiedSchema } from '@mongodb-js/mongodb-schema'; import { JSDOM, VirtualConsole } from 'jsdom'; import parse, { ParseMode } from '@mongodb-js/shell-bson-parser'; diff --git a/packages/mql-typescript/src/driverSchema/static-schemas.ts b/packages/mql-typescript/src/driverSchema/static-schemas.ts index 9452aeed0..c807500aa 100644 --- a/packages/mql-typescript/src/driverSchema/static-schemas.ts +++ b/packages/mql-typescript/src/driverSchema/static-schemas.ts @@ -1,4 +1,4 @@ -import type { SimplifiedSchema } from 'mongodb-schema'; +import type { SimplifiedSchema } from '@mongodb-js/mongodb-schema'; interface SchemaInfo { collectionName: string; diff --git a/packages/mql-typescript/src/testGenerator/test-generator.ts b/packages/mql-typescript/src/testGenerator/test-generator.ts index 96db46d5f..82839d62e 100644 --- a/packages/mql-typescript/src/testGenerator/test-generator.ts +++ b/packages/mql-typescript/src/testGenerator/test-generator.ts @@ -8,7 +8,7 @@ import type { SimplifiedSchema, SimplifiedSchemaType, SimplifiedSchemaBaseType, -} from 'mongodb-schema'; +} from '@mongodb-js/mongodb-schema'; import { unsupportedAggregations } from './unsupported-aggregations'; import * as bson from 'bson'; From 197e4410cb730657e97d820b4596546f89ce37ab Mon Sep 17 00:00:00 2001 From: Eduarda Scharnhorst Date: Tue, 28 Jul 2026 15:10:39 +0100 Subject: [PATCH 2/2] chore: fix dependency order in package.json --- packages/mql-typescript/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mql-typescript/package.json b/packages/mql-typescript/package.json index 007346425..fe2069348 100644 --- a/packages/mql-typescript/package.json +++ b/packages/mql-typescript/package.json @@ -71,8 +71,8 @@ }, "devDependencies": { "@mongodb-js/eslint-config-devtools": "^0.11.8", - "@mongodb-js/prettier-config-devtools": "^1.0.4", "@mongodb-js/mongodb-schema": "^0.1.0", + "@mongodb-js/prettier-config-devtools": "^1.0.4", "@mongodb-js/shell-bson-parser": "^1.5.14", "@mongodb-js/tsconfig-devtools": "^1.1.3", "@types/chai": "^4.2.21",