From 866d64eb7c6ad0d05ecfcf719b6b92ef6486d5a4 Mon Sep 17 00:00:00 2001 From: Rayhan Hossain Date: Thu, 30 Jul 2026 10:04:00 -0700 Subject: [PATCH 1/2] Add MongoDB Node Driver + DocumentDB Playground Signed-off-by: Rayhan Hossain --- README.md | 23 +- playgrounds/mongodb-node/README.md | 171 +++ playgrounds/mongodb-node/app/db.js | 43 + .../mongodb-node/app/mongodb-crud-test.js | 206 ++++ .../mongodb-node/app/package-lock.json | 980 ++++++++++++++++++ playgrounds/mongodb-node/app/package.json | 18 + playgrounds/mongodb-node/app/server.js | 146 +++ playgrounds/mongodb-node/scripts/lib.sh | 85 ++ playgrounds/mongodb-node/scripts/run-app.sh | 29 + playgrounds/mongodb-node/scripts/run-test.sh | 25 + .../mongodb-node/scripts/start-documentdb.sh | 14 + .../mongodb-node/scripts/stop-documentdb.sh | 9 + 12 files changed, 1742 insertions(+), 7 deletions(-) create mode 100644 playgrounds/mongodb-node/README.md create mode 100644 playgrounds/mongodb-node/app/db.js create mode 100644 playgrounds/mongodb-node/app/mongodb-crud-test.js create mode 100644 playgrounds/mongodb-node/app/package-lock.json create mode 100644 playgrounds/mongodb-node/app/package.json create mode 100644 playgrounds/mongodb-node/app/server.js create mode 100755 playgrounds/mongodb-node/scripts/lib.sh create mode 100755 playgrounds/mongodb-node/scripts/run-app.sh create mode 100755 playgrounds/mongodb-node/scripts/run-test.sh create mode 100755 playgrounds/mongodb-node/scripts/start-documentdb.sh create mode 100755 playgrounds/mongodb-node/scripts/stop-documentdb.sh diff --git a/README.md b/README.md index 40d4f41..91f7c2b 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ playground's README states what it needs. ## Playgrounds -| Playground | Language / Stack | What it shows | -| --------------------------------- | ---------------------- | ------------------------------------------------------------------------- | -| [mongoose](playgrounds/mongoose/) | Node.js — Mongoose ODM | Express REST API + a CRUD/compatibility test suite using the Mongoose ODM. | -| [beanie](playgrounds/beanie/) | Python — Beanie ODM | FastAPI REST API + a CRUD/compatibility test suite using the Beanie ODM. | -| [pymongo](playgrounds/pymongo/) | Python — PyMongo driver | Flask REST API + a CRUD/compatibility test suite using the raw PyMongo driver. | +| Playground | Language / Stack | What it shows | +| --------------------------------------- | ------------------------ | -------------------------------------------------------------------------------- | +| [mongoose](playgrounds/mongoose/) | Node.js — Mongoose ODM | Express REST API + a CRUD/compatibility test suite using the Mongoose ODM. | +| [mongodb-node](playgrounds/mongodb-node/) | Node.js — MongoDB driver | Express REST API + a CRUD/compatibility suite using the native Node.js driver. | +| [beanie](playgrounds/beanie/) | Python — Beanie ODM | FastAPI REST API + a CRUD/compatibility test suite using the Beanie ODM. | +| [pymongo](playgrounds/pymongo/) | Python — PyMongo driver | Flask REST API + a CRUD/compatibility test suite using the raw PyMongo driver. | -More playgrounds are planned (for example the **MongoDB Node.js native driver** -and other MongoDB drivers). Contributions are welcome. +More MongoDB driver playgrounds are planned. Contributions are welcome. ## Getting Started @@ -38,6 +38,14 @@ cd playgrounds/mongoose ./scripts/run-app.sh # or run the demo REST API ``` +To try the MongoDB Node.js native driver playground: + +```bash +cd playgrounds/mongodb-node +./scripts/run-test.sh # start DocumentDB locally and run the compatibility suite +./scripts/run-app.sh # or run the demo REST API +``` + To try the Beanie playground: ```bash @@ -62,6 +70,7 @@ documentdb-playground/ ├── LICENSE └── playgrounds/ ├── mongoose/ # Node.js + Mongoose ODM + ├── mongodb-node/ # Node.js + MongoDB native driver ├── beanie/ # Python + Beanie ODM └── pymongo/ # Python + PyMongo driver ``` diff --git a/playgrounds/mongodb-node/README.md b/playgrounds/mongodb-node/README.md new file mode 100644 index 0000000..8bbc8a9 --- /dev/null +++ b/playgrounds/mongodb-node/README.md @@ -0,0 +1,171 @@ +# MongoDB Node.js Driver with DocumentDB (local) + +This playground uses the official [MongoDB Node.js driver](https://www.mongodb.com/docs/drivers/node/current/) +against a local [DocumentDB](https://github.com/documentdb/documentdb) instance. +It includes: + +- an **Express REST API** using native collections (`app/server.js`), and +- a standalone **CRUD/compatibility suite** (`app/mongodb-crud-test.js`) that + exercises connection, indexes, inserts, queries, updates, aggregation, + unique-index enforcement, vector search, deletion, and cleanup. + +The Node.js driver is the low-level MongoDB client used directly here, without +an ODM such as Mongoose. DocumentDB runs in Docker while the app and tests run +as local Node.js processes. + +## Prerequisites + +- **Docker** (Docker Desktop or a Docker daemon) +- **Node.js 18+** and **npm** + +## Quick Start + +From `playgrounds/mongodb-node/`, choose either independent operation: + +```bash +# Start DocumentDB and run the compatibility suite. +./scripts/run-test.sh + +# Or start DocumentDB and serve the API on port 3000. +./scripts/run-app.sh +``` + +Both commands reuse the same `documentdb-local` container if it is running. +The API uses the `mongodb_node_demo` database and the suite uses +`mongodb_node_test`, so they can run at the same time. + +Stop and remove DocumentDB when finished: + +```bash +./scripts/stop-documentdb.sh +``` + +Set `KEEP_DB=0` when running the suite to remove the container automatically: + +```bash +KEEP_DB=0 ./scripts/run-test.sh +``` + +## Trying the API + +With `./scripts/run-app.sh` running: + +```bash +curl -s http://localhost:3000/health + +curl -s -X POST http://localhost:3000/books \ + -H 'Content-Type: application/json' \ + -d '{"title":"Dune","author":"Herbert","genres":["sci-fi"],"pages":412,"rating":5}' + +curl -s http://localhost:3000/books | jq . +curl -s 'http://localhost:3000/books?author=Herbert' | jq . +curl -s http://localhost:3000/stats/genres | jq . +``` + +The API also provides `GET`, `PATCH`, and `DELETE /books/:id`. + +## Connecting the Native Driver + +DocumentDB's local gateway uses TLS, a self-signed certificate, and standalone +topology. The client therefore uses these options from `app/db.js`: + +```js +const client = new MongoClient(uri, { + directConnection: true, + tls: true, + tlsAllowInvalidCertificates: true, + serverSelectionTimeoutMS: 10000, +}); +``` + +The playground removes `replicaSet` from supplied URIs because it conflicts +with `directConnection` against the standalone gateway. For production, set +`TLS_INSECURE=false` and configure a trusted CA rather than accepting an +unverified certificate. + +## Scripts + +| Script | Purpose | +| --- | --- | +| `scripts/run-test.sh` | Start DocumentDB, install dependencies, and run the compatibility suite. | +| `scripts/run-app.sh` | Start DocumentDB, install dependencies, and run the Express API. | +| `scripts/start-documentdb.sh` | Start the local container and wait for readiness. | +| `scripts/stop-documentdb.sh` | Stop and remove the local container. | +| `scripts/lib.sh` | Shared lifecycle and connection-string helpers. | + +## Configuration + +| Variable | Default | Description | +| --- | --- | --- | +| `DOCUMENTDB_IMAGE` | `ghcr.io/documentdb/documentdb/documentdb-local:latest` | Local DocumentDB image. | +| `DOCUMENTDB_CONTAINER` | `documentdb-local` | Container name. | +| `DOCUMENTDB_PORT` | `10260` | Loopback host port. | +| `DOCUMENTDB_USERNAME` | `docdbadmin` | Gateway username. | +| `DOCUMENTDB_PASSWORD` | `Documentdb!Local1` | Development password. | +| `MONGO_URI` | Local URI using the defaults above | Driver connection string. | +| `MONGO_DB` | `mongodb_node_demo` or `mongodb_node_test` | App or test database. | +| `TLS_INSECURE` | `true` | Accept the local self-signed certificate. | +| `SERVER_SELECTION_TIMEOUT_MS` | `10000` | App server-selection timeout. | +| `PORT` | `3000` | REST API port. | +| `KEEP_DB` | `1` | Set to `0` to remove DocumentDB after tests. | + +## Running the Suite Manually + +```bash +cd app +npm install +MONGO_URI='mongodb://docdbadmin:Documentdb!Local1@localhost:10260/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true' \ + npm run test:crud +``` + +Expected summary: + +```text +MongoDB Node.js driver DocumentDB compatibility test +==================================================== + ✅ connect + ✅ create indexes + ✅ insertOne + ... + ✅ $vectorSearch returns nearest neighbor + ✅ cleanup (drop collection) +==================================================== +Passed: 16 Failed: 0 +``` + +## Compatibility Notes + +| Native driver feature | Status | Notes | +| --- | --- | --- | +| CRUD and `_id` lookups | Supported | Uses `insertOne`, `findOne`, `updateOne`, and `deleteOne`. | +| Compound and unique indexes | Supported | Index collation is not supported by DocumentDB. | +| Aggregation | Common stages supported | The suite covers `$unwind`, `$group`, and `$sort`. | +| Vector search | Supported | Uses a `cosmosSearch` vector index and `$vectorSearch`. | +| Transactions/change streams | Not covered | The local gateway advertises standalone topology. | + +## Directory Layout + +```text +mongodb-node/ +├── README.md +├── app/ +│ ├── package.json +│ ├── db.js +│ ├── server.js +│ └── mongodb-crud-test.js +└── scripts/ + ├── lib.sh + ├── start-documentdb.sh + ├── stop-documentdb.sh + ├── run-app.sh + └── run-test.sh +``` + +## Troubleshooting + +- If Docker is unreachable, start Docker Desktop or the Docker daemon. +- For selection timeouts, check `docker logs documentdb-local` and verify port + `10260` is reachable on `localhost`. +- If credentials change, remove the old container before restarting it because + credentials are set when the container is created. +- Omit index `collation`; DocumentDB does not implement it. \ No newline at end of file diff --git a/playgrounds/mongodb-node/app/db.js b/playgrounds/mongodb-node/app/db.js new file mode 100644 index 0000000..4ed5d2e --- /dev/null +++ b/playgrounds/mongodb-node/app/db.js @@ -0,0 +1,43 @@ +'use strict'; + +const { MongoClient } = require('mongodb'); + +const DEFAULT_URI = + 'mongodb://docdbadmin:Documentdb!Local1@localhost:10260/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true'; + +let client; +let database; + +function sanitizeUri(uri = DEFAULT_URI) { + return uri.replace(/([?&])replicaSet=[^&]*(&|$)/g, (_match, lead, trail) => + lead === '?' && trail === '&' ? '?' : trail === '&' ? lead : '' + ); +} + +function buildOptions() { + return { + directConnection: true, + tls: true, + tlsAllowInvalidCertificates: + (process.env.TLS_INSECURE || 'true').toLowerCase() !== 'false', + serverSelectionTimeoutMS: Number(process.env.SERVER_SELECTION_TIMEOUT_MS || 10000), + }; +} + +async function connect() { + if (database) return database; + + client = new MongoClient(sanitizeUri(process.env.MONGO_URI), buildOptions()); + await client.connect(); + database = client.db(process.env.MONGO_DB || 'mongodb_node_demo'); + await database.command({ ping: 1 }); + return database; +} + +async function close() { + if (client) await client.close(); + client = undefined; + database = undefined; +} + +module.exports = { connect, close, sanitizeUri, buildOptions, DEFAULT_URI }; \ No newline at end of file diff --git a/playgrounds/mongodb-node/app/mongodb-crud-test.js b/playgrounds/mongodb-node/app/mongodb-crud-test.js new file mode 100644 index 0000000..24f53a9 --- /dev/null +++ b/playgrounds/mongodb-node/app/mongodb-crud-test.js @@ -0,0 +1,206 @@ +'use strict'; + +const { MongoClient } = require('mongodb'); + +const { DEFAULT_URI, buildOptions, sanitizeUri } = require('./db'); + +const URI = process.argv[2] || process.env.MONGO_URI || DEFAULT_URI; +const DB_NAME = process.env.MONGO_DB || 'mongodb_node_test'; + +let passed = 0; +let failed = 0; + +async function step(name, action) { + try { + await action(); + passed += 1; + console.log(` \u2705 ${name}`); + } catch (error) { + failed += 1; + console.error(` \u274C ${name}: ${error.message}`); + } +} + +async function run() { + console.log('MongoDB Node.js driver DocumentDB compatibility test'); + console.log('===================================================='); + + const client = new MongoClient(sanitizeUri(URI), { + ...buildOptions(), + serverSelectionTimeoutMS: 15000, + }); + let connected = false; + + await step('connect', async () => { + await client.connect(); + const ping = await client.db('admin').command({ ping: 1 }); + if (ping.ok !== 1) throw new Error('ping did not return ok:1'); + connected = true; + }); + + if (!connected) { + console.error('\nConnection failed; aborting remaining steps.'); + process.exitCode = 1; + return; + } + + const database = client.db(DB_NAME); + const collectionName = `widgets_${Date.now()}`; + const collection = database.collection(collectionName); + const vectorCollectionName = `vectors_${Date.now()}`; + const vectors = database.collection(vectorCollectionName); + let createdId; + + await step('create indexes', async () => { + await collection.createIndexes([ + { key: { sku: 1 }, name: 'sku_unique', unique: true }, + { key: { name: 1, price: -1 }, name: 'name_price' }, + ]); + }); + + await step('insertOne', async () => { + const result = await collection.insertOne({ + sku: 'SKU-001', + name: 'Gizmo', + tags: ['alpha', 'beta'], + price: 9.99, + active: true, + createdAt: new Date(), + }); + createdId = result.insertedId; + if (!createdId) throw new Error('no _id returned'); + }); + + await step('insertMany', async () => { + const result = await collection.insertMany([ + { sku: 'SKU-002', name: 'Gadget', tags: ['beta'], price: 19.5 }, + { sku: 'SKU-003', name: 'Widget', tags: ['alpha', 'gamma'], price: 4.25 }, + ]); + if (result.insertedCount !== 2) { + throw new Error(`expected 2 inserted, got ${result.insertedCount}`); + } + }); + + await step('findOne by _id', async () => { + const document = await collection.findOne({ _id: createdId }); + if (!document || document.sku !== 'SKU-001') { + throw new Error('document not found or mismatched'); + } + }); + + await step('find with filter + sort + limit', async () => { + const documents = await collection + .find({ price: { $gte: 5 } }) + .sort({ price: -1 }) + .limit(10) + .toArray(); + if (documents.length !== 2) throw new Error(`expected 2 docs, got ${documents.length}`); + if (documents[0].price < documents[1].price) throw new Error('sort order incorrect'); + }); + + await step('countDocuments', async () => { + const count = await collection.countDocuments({}); + if (count !== 3) throw new Error(`expected 3 docs, got ${count}`); + }); + + await step('updateOne ($set)', async () => { + const result = await collection.updateOne({ sku: 'SKU-002' }, { $set: { price: 21 } }); + if (result.modifiedCount !== 1) { + throw new Error(`expected 1 modified, got ${result.modifiedCount}`); + } + }); + + await step('findOneAndUpdate (returns new)', async () => { + const document = await collection.findOneAndUpdate( + { sku: 'SKU-003' }, + { $push: { tags: 'delta' } }, + { returnDocument: 'after' } + ); + if (!document || !document.tags.includes('delta')) throw new Error('update not applied'); + }); + + await step('aggregation ($unwind/$group)', async () => { + const stats = await collection + .aggregate([ + { $unwind: '$tags' }, + { $group: { _id: '$tags', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + ]) + .toArray(); + if (stats.length === 0) throw new Error('aggregation returned no results'); + }); + + await step('unique index enforcement (duplicate sku rejected)', async () => { + try { + await collection.insertOne({ sku: 'SKU-001', name: 'Duplicate' }); + } catch (error) { + if (error.code === 11000) return; + throw error; + } + throw new Error('duplicate insert was not rejected'); + }); + + await step('deleteOne', async () => { + const result = await collection.deleteOne({ sku: 'SKU-002' }); + if (result.deletedCount !== 1) { + throw new Error(`expected 1 deleted, got ${result.deletedCount}`); + } + }); + + await step('vector index + insert (cosmosSearch vector-ivf)', async () => { + await vectors.insertMany([ + { name: 'a', vector: [1, 0, 0] }, + { name: 'b', vector: [0.9, 0.1, 0] }, + { name: 'c', vector: [0, 0, 1] }, + ]); + const result = await database.command({ + createIndexes: vectorCollectionName, + indexes: [ + { + name: 'vector_ivf', + key: { vector: 'cosmosSearch' }, + cosmosSearchOptions: { + kind: 'vector-ivf', + numLists: 1, + similarity: 'COS', + dimensions: 3, + }, + }, + ], + }); + if (result.ok !== 1) throw new Error('createIndexes did not return ok:1'); + }); + + await step('$vectorSearch returns nearest neighbor', async () => { + const hits = await vectors + .aggregate([ + { + $vectorSearch: { + index: 'vector_ivf', + path: 'vector', + queryVector: [1, 0, 0], + numCandidates: 10, + limit: 2, + }, + }, + { $project: { name: 1, _id: 0 } }, + ]) + .toArray(); + if (hits.length === 0) throw new Error('vector search returned no results'); + if (hits[0].name !== 'a') throw new Error(`expected nearest 'a', got '${hits[0].name}'`); + }); + + await step('vector cleanup (drop collection)', async () => vectors.drop()); + await step('cleanup (drop collection)', async () => collection.drop()); + + await client.close(); + + console.log('\n===================================================='); + console.log(`Passed: ${passed} Failed: ${failed}`); + process.exitCode = failed === 0 ? 0 : 1; +} + +run().catch((error) => { + console.error('Unexpected error:', error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/playgrounds/mongodb-node/app/package-lock.json b/playgrounds/mongodb-node/app/package-lock.json new file mode 100644 index 0000000..470345e --- /dev/null +++ b/playgrounds/mongodb-node/app/package-lock.json @@ -0,0 +1,980 @@ +{ + "name": "documentdb-mongodb-node-playground", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "documentdb-mongodb-node-playground", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "express": "^4.21.2", + "mongodb": "^6.12.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.13.tgz", + "integrity": "sha512-E3Sv4eCYAlKYUTx8S3ioQcDUscOif+8zZ5OnW1IzJ+Tt+EO+ke8mn+Y3FX6N1H79picwbdOavVOb1jPi2EOyrg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "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-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==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/playgrounds/mongodb-node/app/package.json b/playgrounds/mongodb-node/app/package.json new file mode 100644 index 0000000..109e470 --- /dev/null +++ b/playgrounds/mongodb-node/app/package.json @@ -0,0 +1,18 @@ +{ + "name": "documentdb-mongodb-node-playground", + "version": "1.0.0", + "description": "MongoDB Node.js native driver + Express demo and CRUD test suite running against DocumentDB", + "license": "MIT", + "type": "commonjs", + "engines": { + "node": ">=18" + }, + "scripts": { + "start": "node server.js", + "test:crud": "node mongodb-crud-test.js" + }, + "dependencies": { + "express": "^4.21.2", + "mongodb": "^6.12.0" + } +} \ No newline at end of file diff --git a/playgrounds/mongodb-node/app/server.js b/playgrounds/mongodb-node/app/server.js new file mode 100644 index 0000000..c7724ba --- /dev/null +++ b/playgrounds/mongodb-node/app/server.js @@ -0,0 +1,146 @@ +'use strict'; + +const express = require('express'); +const { ObjectId } = require('mongodb'); + +const db = require('./db'); + +const app = express(); +app.use(express.json()); + +const PORT = Number(process.env.PORT || 3000); +let database; +let books; + +function parseObjectId(value) { + return ObjectId.isValid(value) ? new ObjectId(value) : null; +} + +function serialize(document) { + if (!document) return document; + return { ...document, _id: document._id.toHexString() }; +} + +app.get('/health', async (_req, res) => { + try { + const result = await database.command({ ping: 1 }); + if (result.ok === 1) return res.json({ status: 'healthy', db: 'connected' }); + } catch (error) { + return res.status(503).json({ status: 'unhealthy', db: error.message }); + } + return res.status(503).json({ status: 'unhealthy', db: 'disconnected' }); +}); + +app.post('/books', async (req, res) => { + try { + if (!req.body.title || !req.body.author) { + return res.status(400).json({ error: 'title and author are required' }); + } + const now = new Date(); + const document = { + ...req.body, + genres: req.body.genres || [], + inStock: req.body.inStock ?? true, + createdAt: now, + updatedAt: now, + }; + const result = await books.insertOne(document); + res.status(201).json(serialize({ ...document, _id: result.insertedId })); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +app.get('/books', async (req, res) => { + try { + const filter = req.query.author ? { author: req.query.author } : {}; + const documents = await books.find(filter).sort({ createdAt: -1 }).limit(100).toArray(); + res.json({ count: documents.length, books: documents.map(serialize) }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.get('/books/:id', async (req, res) => { + try { + const id = parseObjectId(req.params.id); + if (!id) return res.status(400).json({ error: 'invalid id' }); + const book = await books.findOne({ _id: id }); + if (!book) return res.status(404).json({ error: 'not found' }); + res.json(serialize(book)); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.patch('/books/:id', async (req, res) => { + try { + const id = parseObjectId(req.params.id); + if (!id) return res.status(400).json({ error: 'invalid id' }); + const allowed = ['title', 'author', 'genres', 'pages', 'published', 'inStock', 'rating']; + const changes = Object.fromEntries( + Object.entries(req.body).filter(([key]) => allowed.includes(key)) + ); + changes.updatedAt = new Date(); + const result = await books.findOneAndUpdate( + { _id: id }, + { $set: changes }, + { returnDocument: 'after' } + ); + if (!result) return res.status(404).json({ error: 'not found' }); + res.json(serialize(result)); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +app.delete('/books/:id', async (req, res) => { + try { + const id = parseObjectId(req.params.id); + if (!id) return res.status(400).json({ error: 'invalid id' }); + const result = await books.deleteOne({ _id: id }); + if (result.deletedCount === 0) return res.status(404).json({ error: 'not found' }); + res.status(204).end(); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.get('/stats/genres', async (_req, res) => { + try { + const stats = await books + .aggregate([ + { $unwind: '$genres' }, + { $group: { _id: '$genres', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + ]) + .toArray(); + res.json(stats); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +async function main() { + database = await db.connect(); + books = database.collection('books'); + await books.createIndex({ author: 1, title: 1 }); + + console.log('Connected to DocumentDB via the MongoDB Node.js driver'); + const server = app.listen(PORT, () => + console.log(`MongoDB Node.js driver demo API listening on :${PORT}`) + ); + + const shutdown = (signal) => { + console.log(`Received ${signal}, shutting down...`); + server.close(() => db.close().finally(() => process.exit(0))); + }; + ['SIGTERM', 'SIGINT'].forEach((signal) => + process.on(signal, () => shutdown(signal)) + ); +} + +main().catch((error) => { + console.error('Fatal startup error:', error.message); + process.exit(1); +}); \ No newline at end of file diff --git a/playgrounds/mongodb-node/scripts/lib.sh b/playgrounds/mongodb-node/scripts/lib.sh new file mode 100755 index 0000000..cfcdadd --- /dev/null +++ b/playgrounds/mongodb-node/scripts/lib.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Shared local DocumentDB lifecycle helpers for the MongoDB Node.js driver playground. +set -euo pipefail + +DOCUMENTDB_IMAGE="${DOCUMENTDB_IMAGE:-ghcr.io/documentdb/documentdb/documentdb-local:latest}" +DOCUMENTDB_CONTAINER="${DOCUMENTDB_CONTAINER:-documentdb-local}" +DOCUMENTDB_PORT="${DOCUMENTDB_PORT:-10260}" +DOCUMENTDB_USERNAME="${DOCUMENTDB_USERNAME:-docdbadmin}" +DOCUMENTDB_PASSWORD="${DOCUMENTDB_PASSWORD:-Documentdb!Local1}" + +build_uri() { + echo "mongodb://${DOCUMENTDB_USERNAME}:${DOCUMENTDB_PASSWORD}@localhost:${DOCUMENTDB_PORT}/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" +} + +require_docker() { + command -v docker >/dev/null || { + echo "docker is required (start Docker Desktop / the Docker daemon first)" >&2 + exit 1 + } + docker info >/dev/null 2>&1 || { + echo "Cannot reach the Docker daemon. Is Docker running?" >&2 + exit 1 + } +} + +container_running() { + [ "$(docker inspect -f '{{.State.Running}}' "$DOCUMENTDB_CONTAINER" 2>/dev/null)" = "true" ] +} + +container_exists() { + docker inspect "$DOCUMENTDB_CONTAINER" >/dev/null 2>&1 +} + +ensure_documentdb() { + require_docker + + if container_running; then + echo "DocumentDB container '$DOCUMENTDB_CONTAINER' is already running." + else + if container_exists; then + docker rm -f "$DOCUMENTDB_CONTAINER" >/dev/null 2>&1 || true + fi + + echo "Starting DocumentDB container '$DOCUMENTDB_CONTAINER' on port ${DOCUMENTDB_PORT} ..." + docker run -dt \ + -p "127.0.0.1:${DOCUMENTDB_PORT}:10260" \ + --name "$DOCUMENTDB_CONTAINER" \ + "$DOCUMENTDB_IMAGE" \ + --username "$DOCUMENTDB_USERNAME" \ + --password "$DOCUMENTDB_PASSWORD" >/dev/null + fi + + wait_for_documentdb +} + +wait_for_documentdb() { + local uri + uri="$(build_uri)" + echo "Waiting for DocumentDB to accept connections ..." + + local attempt + for attempt in $(seq 1 60); do + if docker exec "$DOCUMENTDB_CONTAINER" mongosh "$uri" \ + --quiet --eval 'db.adminCommand({ ping: 1 })' >/dev/null 2>&1; then + echo "DocumentDB is ready." + return 0 + fi + sleep 2 + done + + echo "DocumentDB did not become ready in time." >&2 + echo "Check logs with: docker logs $DOCUMENTDB_CONTAINER" >&2 + return 1 +} + +stop_documentdb() { + require_docker + if container_exists; then + echo "Removing DocumentDB container '$DOCUMENTDB_CONTAINER' ..." + docker rm -f "$DOCUMENTDB_CONTAINER" >/dev/null + echo "Done." + else + echo "No DocumentDB container named '$DOCUMENTDB_CONTAINER' found." + fi +} \ No newline at end of file diff --git a/playgrounds/mongodb-node/scripts/run-app.sh b/playgrounds/mongodb-node/scripts/run-app.sh new file mode 100755 index 0000000..d362d37 --- /dev/null +++ b/playgrounds/mongodb-node/scripts/run-app.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Run the MongoDB Node.js native driver demo API against local DocumentDB. +set -euo pipefail + +command -v node >/dev/null || { echo "node is required" >&2; exit 1; } +command -v npm >/dev/null || { echo "npm is required" >&2; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +APP_DIR="$SCRIPT_DIR/../app" +PORT="${PORT:-3000}" + +ensure_documentdb + +echo "Installing app dependencies (express, mongodb)..." +(cd "$APP_DIR" && npm install --omit=dev --no-audit --no-fund >/dev/null) + +echo "" +echo "=== MongoDB Node.js driver demo API running locally ===" +echo "API: http://localhost:${PORT}" +echo "Health: curl http://localhost:${PORT}/health" +echo "Create: curl -X POST http://localhost:${PORT}/books -H 'Content-Type: application/json' \\" +echo " -d '{\"title\":\"Dune\",\"author\":\"Herbert\",\"genres\":[\"sci-fi\"],\"pages\":412}'" +echo "Press Ctrl-C to stop (DocumentDB keeps running; stop it with ./scripts/stop-documentdb.sh)." +echo "" + +MONGO_URI="$(build_uri)" PORT="$PORT" node "$APP_DIR/server.js" \ No newline at end of file diff --git a/playgrounds/mongodb-node/scripts/run-test.sh b/playgrounds/mongodb-node/scripts/run-test.sh new file mode 100755 index 0000000..f96952a --- /dev/null +++ b/playgrounds/mongodb-node/scripts/run-test.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Run the MongoDB Node.js native driver compatibility suite against local DocumentDB. +set -euo pipefail + +command -v node >/dev/null || { echo "node is required" >&2; exit 1; } +command -v npm >/dev/null || { echo "npm is required" >&2; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +APP_DIR="$SCRIPT_DIR/../app" +KEEP_DB="${KEEP_DB:-1}" + +ensure_documentdb + +if [ "$KEEP_DB" != "1" ]; then + trap 'stop_documentdb' EXIT +fi + +echo "Installing test dependencies (mongodb)..." +(cd "$APP_DIR" && npm install --omit=dev --no-audit --no-fund >/dev/null) + +echo "" +MONGO_URI="$(build_uri)" node "$APP_DIR/mongodb-crud-test.js" \ No newline at end of file diff --git a/playgrounds/mongodb-node/scripts/start-documentdb.sh b/playgrounds/mongodb-node/scripts/start-documentdb.sh new file mode 100755 index 0000000..3da6127 --- /dev/null +++ b/playgrounds/mongodb-node/scripts/start-documentdb.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Start a local DocumentDB instance in Docker and wait until it is ready. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +ensure_documentdb + +echo "" +echo "DocumentDB is running locally." +echo " Connection string: $(build_uri)" +echo " Stop it with: ./scripts/stop-documentdb.sh" \ No newline at end of file diff --git a/playgrounds/mongodb-node/scripts/stop-documentdb.sh b/playgrounds/mongodb-node/scripts/stop-documentdb.sh new file mode 100755 index 0000000..e79aabd --- /dev/null +++ b/playgrounds/mongodb-node/scripts/stop-documentdb.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Stop and remove the local DocumentDB container. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +stop_documentdb \ No newline at end of file From 2545499bc93dfffada7ed18f0afde5f44754b0a4 Mon Sep 17 00:00:00 2001 From: Rayhan Hossain Date: Fri, 31 Jul 2026 08:53:24 -0700 Subject: [PATCH 2/2] Address CodeQL feedback for mongo node driver Signed-off-by: Rayhan Hossain --- playgrounds/mongodb-node/README.md | 2 + .../mongodb-node/app/package-lock.json | 52 +++++++++++++++++++ playgrounds/mongodb-node/app/package.json | 1 + playgrounds/mongodb-node/app/server.js | 27 +++++++++- 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/playgrounds/mongodb-node/README.md b/playgrounds/mongodb-node/README.md index 8bbc8a9..c2505f1 100644 --- a/playgrounds/mongodb-node/README.md +++ b/playgrounds/mongodb-node/README.md @@ -107,6 +107,8 @@ unverified certificate. | `TLS_INSECURE` | `true` | Accept the local self-signed certificate. | | `SERVER_SELECTION_TIMEOUT_MS` | `10000` | App server-selection timeout. | | `PORT` | `3000` | REST API port. | +| `RATE_LIMIT_WINDOW_MS` | `60000` | Time window for rate limiting data routes. | +| `RATE_LIMIT_MAX` | `100` | Maximum requests per client within the rate-limit window. | | `KEEP_DB` | `1` | Set to `0` to remove DocumentDB after tests. | ## Running the Suite Manually diff --git a/playgrounds/mongodb-node/app/package-lock.json b/playgrounds/mongodb-node/app/package-lock.json index 470345e..83778c3 100644 --- a/playgrounds/mongodb-node/app/package-lock.json +++ b/playgrounds/mongodb-node/app/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "express": "^4.21.2", + "express-rate-limit": "^8.2.1", "mongodb": "^6.12.0" }, "engines": { @@ -314,6 +315,48 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", + "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express-rate-limit/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -470,6 +513,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/playgrounds/mongodb-node/app/package.json b/playgrounds/mongodb-node/app/package.json index 109e470..3f589a5 100644 --- a/playgrounds/mongodb-node/app/package.json +++ b/playgrounds/mongodb-node/app/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "express": "^4.21.2", + "express-rate-limit": "^8.2.1", "mongodb": "^6.12.0" } } \ No newline at end of file diff --git a/playgrounds/mongodb-node/app/server.js b/playgrounds/mongodb-node/app/server.js index c7724ba..acbd75a 100644 --- a/playgrounds/mongodb-node/app/server.js +++ b/playgrounds/mongodb-node/app/server.js @@ -1,6 +1,7 @@ 'use strict'; const express = require('express'); +const { rateLimit } = require('express-rate-limit'); const { ObjectId } = require('mongodb'); const db = require('./db'); @@ -8,12 +9,30 @@ const db = require('./db'); const app = express(); app.use(express.json()); +const dataRouteLimiter = rateLimit({ + windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS || 60000), + limit: Number(process.env.RATE_LIMIT_MAX || 100), + standardHeaders: 'draft-7', + legacyHeaders: false, +}); +app.use(['/books', '/stats'], dataRouteLimiter); + const PORT = Number(process.env.PORT || 3000); let database; let books; function parseObjectId(value) { - return ObjectId.isValid(value) ? new ObjectId(value) : null; + if (typeof value !== 'string' || !/^[0-9a-fA-F]{24}$/.test(value)) return null; + return ObjectId.createFromHexString(value); +} + +function parseAuthor(value) { + if (value === undefined) return undefined; + if (typeof value !== 'string') return null; + + const author = value.trim(); + if (author.length === 0 || author.length > 100) return null; + return author; } function serialize(document) { @@ -53,7 +72,11 @@ app.post('/books', async (req, res) => { app.get('/books', async (req, res) => { try { - const filter = req.query.author ? { author: req.query.author } : {}; + const author = parseAuthor(req.query.author); + if (author === null) { + return res.status(400).json({ error: 'author must be a non-empty string up to 100 characters' }); + } + const filter = author === undefined ? {} : { author }; const documents = await books.find(filter).sort({ createdAt: -1 }).limit(100).toArray(); res.json({ count: documents.length, books: documents.map(serialize) }); } catch (error) {