From 8521785450abf1223fd820826956c8c21c8bfea2 Mon Sep 17 00:00:00 2001 From: Rayhan Hossain Date: Wed, 1 Jul 2026 13:40:33 -0700 Subject: [PATCH] Add Mongoose with DocumentDB backend playground Signed-off-by: Rayhan Hossain --- documentdb-playground/mongoose/README.md | 312 +++++ .../mongoose/app/.dockerignore | 4 + documentdb-playground/mongoose/app/Dockerfile | 20 + documentdb-playground/mongoose/app/db.js | 56 + .../mongoose/app/models/book.js | 24 + .../mongoose/app/mongoose-crud-test.js | 216 ++++ .../mongoose/app/package-lock.json | 1067 +++++++++++++++++ .../mongoose/app/package.json | 18 + documentdb-playground/mongoose/app/server.js | 113 ++ .../mongoose/documentdb.yaml | 48 + .../mongoose/k8s/mongoose-app.yaml | 83 ++ .../mongoose/scripts/cleanup.sh | 20 + .../mongoose/scripts/deploy.sh | 81 ++ documentdb-playground/mongoose/scripts/lib.sh | 95 ++ .../mongoose/scripts/run-app.sh | 48 + .../mongoose/scripts/run-test.sh | 38 + 16 files changed, 2243 insertions(+) create mode 100644 documentdb-playground/mongoose/README.md create mode 100644 documentdb-playground/mongoose/app/.dockerignore create mode 100644 documentdb-playground/mongoose/app/Dockerfile create mode 100644 documentdb-playground/mongoose/app/db.js create mode 100644 documentdb-playground/mongoose/app/models/book.js create mode 100644 documentdb-playground/mongoose/app/mongoose-crud-test.js create mode 100644 documentdb-playground/mongoose/app/package-lock.json create mode 100644 documentdb-playground/mongoose/app/package.json create mode 100644 documentdb-playground/mongoose/app/server.js create mode 100644 documentdb-playground/mongoose/documentdb.yaml create mode 100644 documentdb-playground/mongoose/k8s/mongoose-app.yaml create mode 100755 documentdb-playground/mongoose/scripts/cleanup.sh create mode 100755 documentdb-playground/mongoose/scripts/deploy.sh create mode 100755 documentdb-playground/mongoose/scripts/lib.sh create mode 100755 documentdb-playground/mongoose/scripts/run-app.sh create mode 100755 documentdb-playground/mongoose/scripts/run-test.sh diff --git a/documentdb-playground/mongoose/README.md b/documentdb-playground/mongoose/README.md new file mode 100644 index 00000000..bee91380 --- /dev/null +++ b/documentdb-playground/mongoose/README.md @@ -0,0 +1,312 @@ +# Mongoose with DocumentDB + +This playground shows how to use [Mongoose](https://mongoosejs.com/), the most +popular MongoDB ODM for Node.js, against DocumentDB. It includes: + +- a small **Express + Mongoose REST API** (`app/`), and +- a standalone **Mongoose CRUD/compatibility test suite** + (`app/mongoose-crud-test.js`) that exercises connect, schema/index creation, + insert, query, update, aggregation, unique-index enforcement, and delete. + +**The primary path is to run the app and test suite locally on your machine**, +connecting to a DocumentDB instance running in a cluster (local kind or remote +AKS) through a `kubectl port-forward`. No image build or in-cluster deployment +is required. Deploying the app *into* the cluster is an optional path documented +later. + +Use it to validate that your application's Mongoose models behave correctly +against the DocumentDB gateway. + +> **What is Mongoose?** Mongoose is a **Node.js library** (an ODM, Object Data +> Modeling layer), not a CLI tool or a server. Your application imports it to +> define schemas/models and talk to a MongoDB-compatible database. In this +> playground the library is used by two things: the demo **app** +> ([`app/server.js`](app/server.js)) and the standalone **test script** +> ([`app/mongoose-crud-test.js`](app/mongoose-crud-test.js)). Configure both via +> the environment variables in [Configuration Reference](#configuration-reference). + +## Architecture + +The app runs **locally**; only DocumentDB runs in the cluster. A `kubectl +port-forward` bridges `localhost` to the in-cluster gateway service. + +``` + Your machine Kubernetes cluster (kind or AKS) +┌────────────────────┐ ┌──────────────────────────────────────┐ +│ mongoose app / │ kubectl │ ┌────────────────┐ ┌────────────┐ │ +│ test script │ port-forward │ │ DocumentDB │──▶│ PostgreSQL │ │ +│ (Node + Mongoose) │──────────────▶│ │ (Gateway) │ │ (CNPG) │ │ +│ localhost:10260 │ (TLS, wire │ └────────────────┘ └────────────┘ │ +└────────────────────┘ protocol) └──────────────────────────────────────┘ +``` + +Mongoose connects to the DocumentDB gateway exactly as it would to a standalone +`mongod`, with three required tweaks (see [Connecting Mongoose to +DocumentDB](#connecting-mongoose-to-documentdb)). + +## Prerequisites + +- `node` + `npm` (to run the app and test suite locally), **primary path** +- A Kubernetes cluster with the [DocumentDB operator](../../README.md) installed + (local kind, or AKS; see [`../aks-setup/`](../aks-setup/README.md)) +- `kubectl` configured to point at that cluster +- `docker` + `kind`, **only** for the optional in-cluster deploy path + +## Quick Start (run app locally) + +> **Before applying `documentdb.yaml`**, edit it and replace the placeholder +> password (`ChangeMe!ReplaceBeforeUsing`) with a real one. + +```bash +# 1. Deploy a DocumentDB instance into your cluster (skip if you already have one). +# Works the same whether the cluster is local kind or remote AKS. +kubectl create namespace documentdb-test +kubectl apply -f documentdb.yaml +kubectl wait --for=jsonpath='{.status.status}'="Cluster in healthy state" \ + documentdb/documentdb-cluster -n documentdb-test --timeout=300s + +# 2. Run the Mongoose app LOCALLY (port-forwards DocumentDB for you). +# Leave this running; open a second terminal for the curl/test commands. +./scripts/run-app.sh + +# 3. In another terminal, run the Mongoose CRUD/compatibility test suite LOCALLY. +./scripts/run-test.sh +``` + +`scripts/run-app.sh` and `scripts/run-test.sh` default to +`DOCUMENTDB_NAMESPACE=documentdb-test` and +`DOCUMENTDB_CLUSTER=documentdb-cluster`. Override via env vars if your cluster +uses different names: + +```bash +DOCUMENTDB_NAMESPACE=my-ns DOCUMENTDB_CLUSTER=my-cluster ./scripts/run-app.sh +``` + +Both scripts work identically against a local kind cluster or a remote AKS +cluster, they only rely on your current `kubectl` context. + +## Trying the API + +With `./scripts/run-app.sh` running, the API is on `http://localhost:3000`: + +```bash +# Health +curl -s http://localhost:3000/health +# {"status":"healthy","db":"connected"} + +# Create a book +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}' + +# List books +curl -s http://localhost:3000/books | jq . + +# Count books per genre (aggregation) +curl -s http://localhost:3000/stats/genres | jq . +``` + +## Optional: Deploy the App In-Cluster + +If you want the app running *inside* the cluster instead of on your machine +(e.g. to measure in-cluster latency, or to avoid a long-lived port-forward), +use `scripts/deploy.sh`. This builds a container image, loads it into kind, +wires up the connection string as a Secret, and deploys a Deployment + Service. + +```bash +./scripts/deploy.sh +kubectl port-forward svc/mongoose-demo -n mongoose-demo 3000:3000 # to reach it +./scripts/cleanup.sh # remove it +``` + +> **AKS note:** `deploy.sh` auto-loads the image into **kind** only. On AKS, +> nodes can't see a locally built image; push it to a registry (e.g. ACR) and +> set `IMAGE` to that pushed tag before running `deploy.sh`. For most testing, +> the local path above avoids this entirely. + +## Connecting Mongoose to DocumentDB + +The DocumentDB gateway speaks the MongoDB wire protocol but advertises itself as +a **standalone** server over **TLS**. Mongoose therefore needs three options +(see [`app/db.js`](app/db.js)): + +```js +await mongoose.connect(uri, { + directConnection: true, // gateway is standalone, not a replica set + tls: true, // gateway only accepts TLS + tlsAllowInvalidCertificates: true, // default install uses a self-signed cert +}); +``` + +Additionally, **strip `replicaSet=rs0`** from the connection string. The +operator-supplied connection string sets both `directConnection=true` and +`replicaSet=rs0`; the Node driver treats these as conflicting and fails with +`client is configured to connect to a replica set named 'rs0' but this node +belongs to a set named 'None'`. Both `app/db.js` and the deploy scripts strip it +automatically. + +For production, set `TLS_INSECURE=false`, mount the cluster CA, and pass +`tlsCAFile` instead of `tlsAllowInvalidCertificates`. + +## Configuration Reference + +All settings are passed via environment variables; there is no config file. + +### App + test script (`app/`) + +Read by [`app/db.js`](app/db.js), [`app/server.js`](app/server.js), and +[`app/mongoose-crud-test.js`](app/mongoose-crud-test.js). + +| Variable | Default | Used by | Description | +| ----------------------------- | ---------------- | -------------- | -------------------------------------------------------------------------------------------- | +| `MONGO_URI` | _(required)_ | app + test | DocumentDB connection string. `replicaSet=rs0` is stripped automatically. The test script also accepts it as the first CLI argument. | +| `MONGO_DB` | `mongoose_demo` (app), `mongoose_test` (test) | app + test | Database name Mongoose connects to. | +| `TLS_INSECURE` | `true` | app + test | When `true`, accepts the gateway's self-signed cert (`tlsAllowInvalidCertificates`). Set `false` for CA-verified TLS in production. | +| `SERVER_SELECTION_TIMEOUT_MS` | `10000` | app | How long Mongoose waits to select a server before erroring. | +| `PORT` | `3000` | app | Port the Express API listens on. | + +These map directly to the Mongoose connect options in +[`app/db.js`](app/db.js): `directConnection: true`, `tls: true`, +`tlsAllowInvalidCertificates`, `serverSelectionTimeoutMS`, and `autoIndex`. + +### Scripts (`scripts/`) + +Read by [`deploy.sh`](scripts/deploy.sh), [`run-test.sh`](scripts/run-test.sh), +and [`cleanup.sh`](scripts/cleanup.sh). + +| Variable | Default | Used by | Description | +| ---------------------- | -------------------- | -------------------- | --------------------------------------------------------------------- | +| `DOCUMENTDB_NAMESPACE` | `documentdb-test` | run-app, run-test, deploy | Namespace of the DocumentDB resource. | +| `DOCUMENTDB_CLUSTER` | `documentdb-cluster` | run-app, run-test, deploy | Name of the `DocumentDB` custom resource. | +| `LOCAL_PORT` | `10260` | run-app, run-test | Local port used for the temporary `kubectl port-forward`. | +| `PORT` | `3000` | run-app | Local port the Express API listens on. | +| `APP_NAMESPACE` | `mongoose-demo` | deploy, cleanup | Namespace the in-cluster app is deployed into (optional path). | +| `IMAGE` | `documentdb/mongoose-demo:local` | deploy | Image tag to build and deploy (optional path). | +| `KIND_CLUSTER` | _(auto-detected)_ | deploy | kind cluster to load the image into. Set when running multiple clusters. | + +## DocumentDB Compatibility Notes + +| Mongoose feature | Status with DocumentDB | Notes | +| --------------------------- | ---------------------- | --------------------------------------------------------------------- | +| CRUD (`create`/`find`/…) | ✅ Supported | Standard document operations work as expected. | +| `autoIndex` / `syncIndexes` | ✅ Supported | Avoid `collation` on indexes; DocumentDB does not implement them. | +| Unique indexes | ✅ Supported | Duplicate keys raise the standard `E11000` error. | +| Aggregation pipelines | ✅ Common stages | `$match`, `$group`, `$unwind`, `$sort`, etc. Atlas-only stages differ.| +| `findById` / `_id` lookups | ⚠️ Known gateway bug | Filtering on `_id` fails with `trying to open a pruned relation` on the gateway bundled with operator `0.2.0` (gateway `0.109.0`). Fixed upstream but not yet in a released, operator-compatible gateway image. Query by another field as a workaround. | +| `$vectorSearch` | ❌ Not supported | Atlas-only operator; not implemented by DocumentDB. | +| Index `collation` | ❌ Not supported | `createIndex.collation is not implemented yet`; omit it. | +| Change streams | ⚠️ Check version | Verify against your DocumentDB version before relying on it. | + +The CRUD test suite ([`app/mongoose-crud-test.js`](app/mongoose-crud-test.js)) +covers the supported rows above and prints a pass/fail summary. + +## Running the Test Suite Manually + +`scripts/run-test.sh` port-forwards DocumentDB to `localhost` and runs the suite +for you. To run it directly against any reachable connection string (for +example, an AKS DocumentDB exposed via a `LoadBalancer` external IP): + +```bash +cd app +npm install +MONGO_URI="mongodb://user:pass@HOST:10260/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" \ + node mongoose-crud-test.js +``` + +Expected output: + +``` +Mongoose DocumentDB compatibility test +====================================== + ✅ connect + ✅ create indexes (autoIndex / syncIndexes) + ✅ insertOne (Model.create) + ✅ insertMany + ⚠️ findById: known DocumentDB issue (trying to open a pruned relation) + ... +====================================== +Passed: 12 Failed: 0 Known issues: 1 +``` + +> **Note:** On operator `0.2.0` (gateway `0.109.0`) the `findById` step hits a +> known gateway bug for `_id` point lookups (`trying to open a pruned relation`). +> The suite reports it as a **known issue** (⚠️) rather than a failure, so the +> run still exits green (`Failed: 0`). If a fixed gateway release is deployed, +> the step passes and is counted as a normal pass. See the compatibility table +> above and the troubleshooting entry below. + +## What the Scripts Do + +| Script | Purpose | +| ----------------------- | ---------------------------------------------------------------------------------------- | +| `scripts/run-app.sh` | **Primary.** Port-forwards DocumentDB and runs the Express + Mongoose app locally. | +| `scripts/run-test.sh` | **Primary.** Port-forwards DocumentDB and runs the Mongoose CRUD/compatibility suite locally. | +| `scripts/deploy.sh` | Optional. Builds the demo image, loads it into kind, wires the connection string into a Secret, and deploys the app in-cluster. | +| `scripts/cleanup.sh` | Removes the in-cluster app, Secret, and namespace (leaves DocumentDB running). | +| `scripts/lib.sh` | Shared helpers: resolve the `MONGO_URI` from the DocumentDB status and manage the port-forward. | + +## Troubleshooting + +### App pod is `CrashLoopBackOff` with a `replicaSet 'rs0'` error + +The connection string still contains `replicaSet=rs0`. The scripts strip it +automatically; if you set `MONGO_URI` manually, remove that query parameter. + +### `MongooseServerSelectionError` / TLS handshake failures + +The gateway requires TLS. Confirm `tls: true` is set and, for the default +self-signed install, `tlsAllowInvalidCertificates: true` (or `TLS_INSECURE` +unset/`true`). Verify the gateway is reachable: + +```bash +kubectl run mongo-test --rm -it --restart=Never -n documentdb-test --image=mongo:7 \ + --command -- mongosh "" --eval 'db.adminCommand({ping:1})' +``` + +### `trying to open a pruned relation` on `findById` / `_id` queries + +This is a known bug in the gateway bundled with operator `0.2.0` +(gateway `0.109.0`): any query that filters on `_id` (for example Mongoose +`findById` or `GET /books/:id`) fails with this error, while queries on other +fields work. The fix exists upstream but has not yet been published as an +operator-compatible gateway release tag. Until then, look documents up by +another indexed field (for example a unique `sku`/business key). Once a fixed +gateway release is available, pin it via `spec.gatewayImage` in +[`documentdb.yaml`](documentdb.yaml). + +### `createIndex.collation is not implemented yet` + +A schema index uses `collation`. Remove it; DocumentDB does not support +collation indexes. The models in this playground intentionally avoid it. + +### Image not found when deploying to kind + +The deploy script auto-detects the first kind cluster. If you run multiple +clusters, set the target explicitly: + +```bash +KIND_CLUSTER=my-cluster ./scripts/deploy.sh +``` + +## Directory Layout + +``` +mongoose/ +├── README.md +├── documentdb.yaml # Sample DocumentDB instance +├── app/ +│ ├── package.json +│ ├── db.js # Mongoose connection (DocumentDB options) +│ ├── server.js # Express REST API (/books, /health, /stats) +│ ├── models/book.js # Example Mongoose schema/model +│ ├── mongoose-crud-test.js # Standalone CRUD/compatibility test suite +│ └── Dockerfile +├── k8s/ +│ └── mongoose-app.yaml # Deployment + Service (optional in-cluster path) +└── scripts/ + ├── lib.sh # Shared connection-string resolver + port-forward + ├── run-app.sh # Primary: run the app locally + ├── run-test.sh # Primary: run the test suite locally + ├── deploy.sh # Optional: deploy the app in-cluster + └── cleanup.sh +``` diff --git a/documentdb-playground/mongoose/app/.dockerignore b/documentdb-playground/mongoose/app/.dockerignore new file mode 100644 index 00000000..9901d67c --- /dev/null +++ b/documentdb-playground/mongoose/app/.dockerignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log +.env +mongoose-crud-test.js diff --git a/documentdb-playground/mongoose/app/Dockerfile b/documentdb-playground/mongoose/app/Dockerfile new file mode 100644 index 00000000..3b02bd61 --- /dev/null +++ b/documentdb-playground/mongoose/app/Dockerfile @@ -0,0 +1,20 @@ +# Mongoose + Express demo for DocumentDB. +FROM node:20-alpine + +WORKDIR /app + +# Install dependencies first for better layer caching. +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +# Copy application source. +COPY db.js server.js ./ +COPY models ./models + +# Run as the built-in non-root user. +USER node + +ENV PORT=3000 +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/documentdb-playground/mongoose/app/db.js b/documentdb-playground/mongoose/app/db.js new file mode 100644 index 00000000..d3fb96b5 --- /dev/null +++ b/documentdb-playground/mongoose/app/db.js @@ -0,0 +1,56 @@ +'use strict'; + +const mongoose = require('mongoose'); + +/** + * Build the Mongoose connection options that DocumentDB's gateway requires. + * + * DocumentDB exposes a single gateway endpoint that speaks the MongoDB wire + * protocol but advertises itself as a standalone server (not a replica set). + * Mongoose / the Node driver therefore needs: + * + * - directConnection: true -> don't attempt replica-set topology + * discovery (the gateway is standalone). + * - tls: true -> the gateway only accepts TLS. + * - tlsAllowInvalidCertificates -> the default install uses a self-signed + * cert. Set TLS_INSECURE=false and mount a + * CA bundle for production-grade verification. + * + * The connection string itself (MONGO_URI) carries the credentials. If it still + * contains `replicaSet=rs0`, strip it here so the driver does not try to match a + * replica-set name the gateway never advertises. + */ +function sanitizeUri(uri) { + if (!uri) { + throw new Error('MONGO_URI is not set. Provide a DocumentDB connection string.'); + } + // Remove replicaSet=... (incompatible with directConnection against the gateway). + return uri.replace(/([?&])replicaSet=[^&]*(&|$)/g, (_match, lead, trail) => + lead === '?' && trail === '&' ? '?' : trail === '&' ? lead : '' + ); +} + +function buildOptions() { + const tlsInsecure = (process.env.TLS_INSECURE || 'true').toLowerCase() !== 'false'; + return { + directConnection: true, + tls: true, + tlsAllowInvalidCertificates: tlsInsecure, + serverSelectionTimeoutMS: Number(process.env.SERVER_SELECTION_TIMEOUT_MS || 10000), + // DocumentDB does not support server-side index collation. Mongoose's + // autoIndex is fine as long as schemas avoid `collation` on indexes. + autoIndex: true, + }; +} + +async function connect() { + const uri = sanitizeUri(process.env.MONGO_URI); + const dbName = process.env.MONGO_DB || 'mongoose_demo'; + + mongoose.set('strictQuery', true); + + await mongoose.connect(uri, { ...buildOptions(), dbName }); + return mongoose.connection; +} + +module.exports = { connect, sanitizeUri, buildOptions }; diff --git a/documentdb-playground/mongoose/app/models/book.js b/documentdb-playground/mongoose/app/models/book.js new file mode 100644 index 00000000..cbf4a069 --- /dev/null +++ b/documentdb-playground/mongoose/app/models/book.js @@ -0,0 +1,24 @@ +'use strict'; + +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const bookSchema = new Schema( + { + title: { type: String, required: true, trim: true }, + author: { type: String, required: true }, + genres: { type: [String], default: [] }, + pages: { type: Number, min: 1 }, + published: { type: Date }, + inStock: { type: Boolean, default: true }, + rating: { type: Number, min: 0, max: 5 }, + }, + { timestamps: true } +); + +// Compound index: exercises DocumentDB index creation via Mongoose autoIndex. +// Note: no `collation` option here; DocumentDB does not implement collation indexes. +bookSchema.index({ author: 1, title: 1 }); + +module.exports = mongoose.model('Book', bookSchema); diff --git a/documentdb-playground/mongoose/app/mongoose-crud-test.js b/documentdb-playground/mongoose/app/mongoose-crud-test.js new file mode 100644 index 00000000..cdf4d75f --- /dev/null +++ b/documentdb-playground/mongoose/app/mongoose-crud-test.js @@ -0,0 +1,216 @@ +'use strict'; + +/** + * Mongoose CRUD/compatibility test against DocumentDB. + * + * Usage: + * MONGO_URI="mongodb://user:pass@host:10260/?tls=true&tlsAllowInvalidCertificates=true&directConnection=true" \ + * node mongoose-crud-test.js + * + * Or pass the URI as the first argument: + * node mongoose-crud-test.js "mongodb://user:pass@host:10260/?..." + * + * Exercises connect, schema/model registration, index creation, insert, + * find, update, aggregation, and delete using Mongoose against the DocumentDB + * gateway. Exits non-zero on the first failure. + */ + +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const URI = process.argv[2] || process.env.MONGO_URI; +const DB_NAME = process.env.MONGO_DB || 'mongoose_test'; + +let passed = 0; +let failed = 0; +let knownIssues = 0; + +function ok(name) { + passed += 1; + console.log(` \u2705 ${name}`); +} + +function fail(name, err) { + failed += 1; + console.error(` \u274C ${name}: ${err && err.message ? err.message : err}`); +} + +async function step(name, fn) { + try { + await fn(); + ok(name); + } catch (err) { + fail(name, err); + } +} + +// Run a step that is expected to hit a documented DocumentDB limitation. If it +// fails with the known error, report it as a known issue (does not fail the +// suite). If it unexpectedly passes, count it as a pass so this auto-detects a +// fixed release. Any other error is a real failure. +async function stepKnownIssue(name, fn, knownErrorPattern) { + try { + await fn(); + ok(`${name} (known issue resolved)`); + } catch (err) { + const msg = err && err.message ? err.message : String(err); + if (knownErrorPattern.test(msg)) { + knownIssues += 1; + console.warn(` \u26A0\uFE0F ${name}: known DocumentDB issue (${msg})`); + } else { + fail(name, err); + } + } +} + +function sanitizeUri(uri) { + return uri.replace(/([?&])replicaSet=[^&]*(&|$)/g, (_m, lead, trail) => + lead === '?' && trail === '&' ? '?' : trail === '&' ? lead : '' + ); +} + +const widgetSchema = new Schema( + { + sku: { type: String, required: true, unique: true }, + name: { type: String, required: true }, + tags: { type: [String], default: [] }, + price: { type: Number, min: 0 }, + active: { type: Boolean, default: true }, + }, + { timestamps: true } +); +widgetSchema.index({ name: 1, price: -1 }); + +async function run() { + if (!URI) { + console.error('MONGO_URI not provided. Pass it as $1 or set MONGO_URI.'); + process.exit(2); + } + + console.log('Mongoose DocumentDB compatibility test'); + console.log('======================================'); + + const tlsInsecure = (process.env.TLS_INSECURE || 'true').toLowerCase() !== 'false'; + mongoose.set('strictQuery', true); + + await step('connect', async () => { + await mongoose.connect(sanitizeUri(URI), { + dbName: DB_NAME, + directConnection: true, + tls: true, + tlsAllowInvalidCertificates: tlsInsecure, + serverSelectionTimeoutMS: 15000, + }); + const ping = await mongoose.connection.db.admin().command({ ping: 1 }); + if (ping.ok !== 1) throw new Error('ping did not return ok:1'); + }); + + if (mongoose.connection.readyState !== 1) { + console.error('\nConnection failed; aborting remaining steps.'); + process.exit(1); + } + + // Fresh collection per run to keep the test idempotent. + const collName = `widgets_${Date.now()}`; + const Widget = mongoose.model('Widget', widgetSchema, collName); + + await step('create indexes (autoIndex / syncIndexes)', async () => { + await Widget.syncIndexes(); + }); + + let createdId; + await step('insertOne (Model.create)', async () => { + const doc = await Widget.create({ + sku: 'SKU-001', + name: 'Gizmo', + tags: ['alpha', 'beta'], + price: 9.99, + }); + createdId = doc._id; + if (!createdId) throw new Error('no _id returned'); + }); + + await step('insertMany', async () => { + const res = await Widget.insertMany([ + { sku: 'SKU-002', name: 'Gadget', tags: ['beta'], price: 19.5 }, + { sku: 'SKU-003', name: 'Widget', tags: ['alpha', 'gamma'], price: 4.25 }, + ]); + if (res.length !== 2) throw new Error(`expected 2 inserted, got ${res.length}`); + }); + + // Known issue: gateway 0.109.0 (operator 0.2.0) fails point lookups that + // filter on `_id` with "trying to open a pruned relation". Treated as a known + // issue so the suite stays green; auto-detects a fixed release if it passes. + await stepKnownIssue( + 'findById', + async () => { + const doc = await Widget.findById(createdId).lean(); + if (!doc || doc.sku !== 'SKU-001') throw new Error('document not found or mismatched'); + }, + /pruned relation/i + ); + + await step('find with filter + sort + limit', async () => { + const docs = await Widget.find({ price: { $gte: 5 } }).sort({ price: -1 }).limit(10).lean(); + if (docs.length !== 2) throw new Error(`expected 2 docs, got ${docs.length}`); + if (docs[0].price < docs[1].price) throw new Error('sort order incorrect'); + }); + + await step('countDocuments', async () => { + const n = await Widget.countDocuments({}); + if (n !== 3) throw new Error(`expected 3 docs, got ${n}`); + }); + + await step('updateOne ($set)', async () => { + const res = await Widget.updateOne({ sku: 'SKU-002' }, { $set: { price: 21 } }); + if (res.modifiedCount !== 1) throw new Error(`expected 1 modified, got ${res.modifiedCount}`); + }); + + await step('findOneAndUpdate (returns new)', async () => { + const doc = await Widget.findOneAndUpdate( + { sku: 'SKU-003' }, + { $push: { tags: 'delta' } }, + { new: true } + ).lean(); + if (!doc.tags.includes('delta')) throw new Error('update not applied'); + }); + + await step('aggregation ($unwind/$group)', async () => { + const stats = await Widget.aggregate([ + { $unwind: '$tags' }, + { $group: { _id: '$tags', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + ]); + if (!stats.length) throw new Error('aggregation returned no results'); + }); + + await step('unique index enforcement (duplicate sku rejected)', async () => { + try { + await Widget.create({ sku: 'SKU-001', name: 'Duplicate' }); + throw new Error('duplicate insert was not rejected'); + } catch (err) { + if (err.code !== 11000) throw new Error(`expected duplicate-key error (11000), got ${err.message}`); + } + }); + + await step('deleteOne', async () => { + const res = await Widget.deleteOne({ sku: 'SKU-002' }); + if (res.deletedCount !== 1) throw new Error(`expected 1 deleted, got ${res.deletedCount}`); + }); + + await step('cleanup (drop collection)', async () => { + await Widget.collection.drop(); + }); + + await mongoose.disconnect(); + + console.log('\n======================================'); + console.log(`Passed: ${passed} Failed: ${failed} Known issues: ${knownIssues}`); + process.exit(failed === 0 ? 0 : 1); +} + +run().catch((err) => { + console.error('Unexpected error:', err); + process.exit(1); +}); diff --git a/documentdb-playground/mongoose/app/package-lock.json b/documentdb-playground/mongoose/app/package-lock.json new file mode 100644 index 00000000..077ad8a7 --- /dev/null +++ b/documentdb-playground/mongoose/app/package-lock.json @@ -0,0 +1,1067 @@ +{ + "name": "documentdb-mongoose-playground", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "documentdb-mongoose-playground", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "express": "^4.21.2", + "mongoose": "^8.9.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz", + "integrity": "sha512-QAfAMwNgnYxZ2C6D1HgeP7Gc4i/uvJRim415PCIL9ptRxWMNbWeLBYb2/9R4pGKny/s1FVu2JA2cxCUBUOggrA==", + "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.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "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/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "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/mongoose": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.1.tgz", + "integrity": "sha512-UpHBA0l5kHyKJQFjmBaFYQFo5sgz1DK0TRqDkOyBLYbqiIbKKhIvBpHWBXqeo0rgW4kGI1UhhAw+kTQZoj1BdA==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/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/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/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/mquery/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/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/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "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/documentdb-playground/mongoose/app/package.json b/documentdb-playground/mongoose/app/package.json new file mode 100644 index 00000000..f6d0335d --- /dev/null +++ b/documentdb-playground/mongoose/app/package.json @@ -0,0 +1,18 @@ +{ + "name": "documentdb-mongoose-playground", + "version": "1.0.0", + "description": "Mongoose + Express demo and CRUD test suite running against DocumentDB", + "license": "MIT", + "type": "commonjs", + "engines": { + "node": ">=18" + }, + "scripts": { + "start": "node server.js", + "test:crud": "node mongoose-crud-test.js" + }, + "dependencies": { + "express": "^4.21.2", + "mongoose": "^8.9.5" + } +} diff --git a/documentdb-playground/mongoose/app/server.js b/documentdb-playground/mongoose/app/server.js new file mode 100644 index 00000000..cd8a6b0a --- /dev/null +++ b/documentdb-playground/mongoose/app/server.js @@ -0,0 +1,113 @@ +'use strict'; + +const express = require('express'); +const mongoose = require('mongoose'); + +const { connect } = require('./db'); +const Book = require('./models/book'); + +const app = express(); +app.use(express.json()); + +const PORT = Number(process.env.PORT || 3000); + +// Liveness/readiness probe. Returns 200 only when the Mongoose connection is up. +app.get('/health', (req, res) => { + const state = mongoose.connection.readyState; // 1 = connected + if (state === 1) { + return res.json({ status: 'healthy', db: 'connected' }); + } + return res.status(503).json({ status: 'unhealthy', db: mongoose.STATES[state] }); +}); + +// Create a book. +app.post('/books', async (req, res) => { + try { + const book = await Book.create(req.body); + res.status(201).json(book); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// List books, optionally filtered by author. +app.get('/books', async (req, res) => { + try { + const filter = req.query.author ? { author: req.query.author } : {}; + const books = await Book.find(filter).sort({ createdAt: -1 }).limit(100).lean(); + res.json({ count: books.length, books }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Fetch a single book by id. +app.get('/books/:id', async (req, res) => { + try { + const book = await Book.findById(req.params.id).lean(); + if (!book) return res.status(404).json({ error: 'not found' }); + res.json(book); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// Update a book. +app.patch('/books/:id', async (req, res) => { + try { + const book = await Book.findByIdAndUpdate(req.params.id, req.body, { + new: true, + runValidators: true, + }).lean(); + if (!book) return res.status(404).json({ error: 'not found' }); + res.json(book); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// Delete a book. +app.delete('/books/:id', async (req, res) => { + try { + const result = await Book.findByIdAndDelete(req.params.id).lean(); + if (!result) return res.status(404).json({ error: 'not found' }); + res.status(204).end(); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// Simple aggregation: count books per genre. +app.get('/stats/genres', async (req, res) => { + try { + const stats = await Book.aggregate([ + { $unwind: '$genres' }, + { $group: { _id: '$genres', count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + ]); + res.json(stats); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +async function main() { + await connect(); + console.log('Connected to DocumentDB via Mongoose'); + const server = app.listen(PORT, () => + console.log(`Mongoose demo API listening on :${PORT}`) + ); + + const shutdown = (signal) => { + console.log(`Received ${signal}, shutting down...`); + server.close(() => { + mongoose.connection.close(false).finally(() => process.exit(0)); + }); + }; + ['SIGTERM', 'SIGINT'].forEach((sig) => process.on(sig, () => shutdown(sig))); +} + +main().catch((err) => { + console.error('Fatal startup error:', err.message); + process.exit(1); +}); diff --git a/documentdb-playground/mongoose/documentdb.yaml b/documentdb-playground/mongoose/documentdb.yaml new file mode 100644 index 00000000..555646b9 --- /dev/null +++ b/documentdb-playground/mongoose/documentdb.yaml @@ -0,0 +1,48 @@ +# Sample DocumentDB instance sized for the Mongoose playground. +# +# Apply with: +# kubectl create namespace documentdb-test +# kubectl apply -f documentdb.yaml +# +# Notes: +# - Replace the password before using outside a throwaway dev cluster. +# - The gateway speaks the MongoDB wire protocol, so Mongoose connects to it +# exactly like it would to a standalone mongod (with TLS + directConnection). +--- +apiVersion: v1 +kind: Secret +metadata: + name: docdb-credentials + namespace: documentdb-test +type: Opaque +stringData: + username: docdbadmin + password: "ChangeMe!ReplaceBeforeUsing" +--- +apiVersion: documentdb.io/preview +kind: DocumentDB +metadata: + name: documentdb-cluster + namespace: documentdb-test +spec: + nodeCount: 1 + instancesPerNode: 1 + documentDbCredentialSecret: docdb-credentials + resource: + storage: + pvcSize: "10Gi" + exposeViaService: + serviceType: ClusterIP + sidecarInjectorPluginName: cnpg-i-sidecar-injector.documentdb.io + # documentDBVersion pins the version for ALL DocumentDB components (engine + + # gateway) in lockstep. Prefer this over setting gatewayImage/documentDBImage + # individually: explicit per-component image fields take precedence over this + # value, so pinning only one would skew engine/gateway versions. + # + # 0.109.0 is the latest published release (and the operator 0.2.0 default). + # + # Known issue: gateway 0.109.0 fails point lookups that filter on `_id` + # (e.g. Mongoose `findById`) with "trying to open a pruned relation". The fix + # exists upstream but is not yet in a published release. Bump this value once a + # newer DocumentDB version is released. + documentDBVersion: "0.109.0" diff --git a/documentdb-playground/mongoose/k8s/mongoose-app.yaml b/documentdb-playground/mongoose/k8s/mongoose-app.yaml new file mode 100644 index 00000000..ebff30e2 --- /dev/null +++ b/documentdb-playground/mongoose/k8s/mongoose-app.yaml @@ -0,0 +1,83 @@ +# Mongoose demo app: Deployment + Service. +# +# The MONGO_URI is supplied via the `mongoose-app-secret` Secret, which +# scripts/deploy.sh creates from the DocumentDB resource's connection string. +# The image reference is patched by scripts/deploy.sh (default: a locally built +# image loaded into kind). +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mongoose-demo + namespace: mongoose-demo + labels: + app: mongoose-demo +spec: + replicas: 1 + selector: + matchLabels: + app: mongoose-demo + template: + metadata: + labels: + app: mongoose-demo + spec: + containers: + - name: mongoose-demo + image: documentdb/mongoose-demo:local + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3000 + env: + - name: PORT + value: "3000" + - name: MONGO_DB + value: "mongoose_demo" + - name: TLS_INSECURE + value: "true" + - name: MONGO_URI + valueFrom: + secretKeyRef: + name: mongoose-app-secret + key: MONGO_URI + readinessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL +--- +apiVersion: v1 +kind: Service +metadata: + name: mongoose-demo + namespace: mongoose-demo + labels: + app: mongoose-demo +spec: + type: ClusterIP + selector: + app: mongoose-demo + ports: + - port: 3000 + targetPort: 3000 + protocol: TCP diff --git a/documentdb-playground/mongoose/scripts/cleanup.sh b/documentdb-playground/mongoose/scripts/cleanup.sh new file mode 100755 index 00000000..e7238448 --- /dev/null +++ b/documentdb-playground/mongoose/scripts/cleanup.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Remove the Mongoose demo app from the cluster. +set -euo pipefail + +APP_NAMESPACE="${APP_NAMESPACE:-mongoose-demo}" + +echo "=== Cleaning up Mongoose demo deployment ===" + +echo "Deleting app resources..." +kubectl delete deployment mongoose-demo -n "$APP_NAMESPACE" 2>/dev/null || true +kubectl delete service mongoose-demo -n "$APP_NAMESPACE" 2>/dev/null || true +kubectl delete secret mongoose-app-secret -n "$APP_NAMESPACE" 2>/dev/null || true + +echo "Deleting namespace..." +kubectl delete namespace "$APP_NAMESPACE" 2>/dev/null || true + +echo "Cleanup complete." +echo "" +echo "Note: the DocumentDB instance (documentdb.yaml) is left running." +echo "Remove it with: kubectl delete -f documentdb.yaml" diff --git a/documentdb-playground/mongoose/scripts/deploy.sh b/documentdb-playground/mongoose/scripts/deploy.sh new file mode 100755 index 00000000..e6acef11 --- /dev/null +++ b/documentdb-playground/mongoose/scripts/deploy.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Deploy the Mongoose demo app against an existing DocumentDB instance. +# +# Prerequisites: kubectl, docker, a running cluster with the DocumentDB +# operator installed and a DocumentDB instance deployed (see documentdb.yaml). +# If your cluster is kind, the locally built image is loaded automatically. +set -euo pipefail + +command -v kubectl >/dev/null || { echo "kubectl is required" >&2; exit 1; } +command -v docker >/dev/null || { echo "docker 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" +MANIFEST="$SCRIPT_DIR/../k8s/mongoose-app.yaml" + +APP_NAMESPACE="${APP_NAMESPACE:-mongoose-demo}" +DOCUMENTDB_NAMESPACE="${DOCUMENTDB_NAMESPACE:-documentdb-test}" +DOCUMENTDB_CLUSTER="${DOCUMENTDB_CLUSTER:-documentdb-cluster}" +IMAGE="${IMAGE:-documentdb/mongoose-demo:local}" +KIND_CLUSTER="${KIND_CLUSTER:-}" + +echo "=== Mongoose + DocumentDB Deployment ===" + +# 1. Build the demo image. +echo "" +echo "--- Step 1: Build app image ($IMAGE) ---" +docker build -t "$IMAGE" "$APP_DIR" + +# 2. Load the image into kind if a kind cluster is detected/named. +if command -v kind >/dev/null 2>&1; then + if [ -z "$KIND_CLUSTER" ]; then + KIND_CLUSTER=$(kind get clusters 2>/dev/null | head -1 || true) + fi + if [ -n "$KIND_CLUSTER" ]; then + echo "" + echo "--- Step 2: Load image into kind cluster '$KIND_CLUSTER' ---" + kind load docker-image "$IMAGE" --name "$KIND_CLUSTER" + fi +fi + +# 3. Resolve the DocumentDB connection string. +echo "" +echo "--- Step 3: Resolve DocumentDB connection ---" +MONGO_URI=$(resolve_mongo_uri "$DOCUMENTDB_NAMESPACE" "$DOCUMENTDB_CLUSTER") +echo "Connection string resolved from documentdb/$DOCUMENTDB_CLUSTER status." + +# 4. Create namespace + secret. +echo "" +echo "--- Step 4: Create namespace and secret ---" +kubectl create namespace "$APP_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - +kubectl create secret generic mongoose-app-secret \ + --namespace "$APP_NAMESPACE" \ + --from-literal=MONGO_URI="$MONGO_URI" \ + --dry-run=client -o yaml | kubectl apply -f - + +# 5. Deploy the app (patch namespace + image). +echo "" +echo "--- Step 5: Deploy Mongoose demo app ---" +sed -e "s#documentdb/mongoose-demo:local#${IMAGE}#g" \ + -e "s#namespace: mongoose-demo#namespace: ${APP_NAMESPACE}#g" \ + "$MANIFEST" \ + | kubectl apply -f - + +echo "Waiting for the app pod to become ready..." +kubectl rollout status deployment/mongoose-demo -n "$APP_NAMESPACE" --timeout=180s + +echo "" +echo "=== Deployment complete ===" +echo "" +echo "Try the API:" +echo " kubectl port-forward svc/mongoose-demo -n $APP_NAMESPACE 3000:3000" +echo " curl http://localhost:3000/health" +echo " curl -X POST http://localhost:3000/books -H 'Content-Type: application/json' \\" +echo " -d '{\"title\":\"Dune\",\"author\":\"Herbert\",\"genres\":[\"sci-fi\"],\"pages\":412}'" +echo " curl http://localhost:3000/books" +echo "" +echo "Run the Mongoose CRUD test suite against the cluster:" +echo " ./scripts/run-test.sh" diff --git a/documentdb-playground/mongoose/scripts/lib.sh b/documentdb-playground/mongoose/scripts/lib.sh new file mode 100755 index 00000000..c6792f2c --- /dev/null +++ b/documentdb-playground/mongoose/scripts/lib.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Shared helpers for the Mongoose playground scripts. +set -euo pipefail + +# Resolve the in-cluster MongoDB URI from a DocumentDB resource's status. +# +# Args: +# Echoes a connection string that uses the in-cluster DNS name and is safe for +# a direct gateway connection (replicaSet stripped). +resolve_mongo_uri() { + local ns="$1" cluster="$2" + + local raw + raw=$(kubectl get documentdb "$cluster" -n "$ns" \ + -o jsonpath='{.status.connectionString}' 2>/dev/null) || true + if [ -z "$raw" ]; then + echo "Could not read status.connectionString from documentdb/$cluster in $ns" >&2 + return 1 + fi + + # The operator embeds $(kubectl get secret ...) substitutions in the + # connection string. Before eval'ing it, constrain what may be executed to + # exactly that shape: reject backticks and any command substitution that is + # not a `kubectl get secret` lookup. This limits command-injection exposure + # if this is ever pointed at an untrusted DocumentDB resource. + if printf '%s' "$raw" | grep -q '`'; then + echo "Refusing to evaluate connection string containing backticks" >&2 + return 1 + fi + local subst + while IFS= read -r subst; do + case "$subst" in + 'kubectl get secret '*) ;; + *) echo "Refusing to evaluate unexpected command substitution: \$($subst)" >&2; return 1 ;; + esac + done < <(printf '%s\n' "$raw" | grep -oE '\$\([^)]*\)' | sed -E 's/^\$\((.*)\)$/\1/') + + local uri + uri=$(eval "echo \"$raw\"") + + # Replace ClusterIP with the in-cluster DNS name for cross-namespace use. + local svc_ip + svc_ip=$(kubectl get svc "documentdb-service-${cluster}" -n "$ns" \ + -o jsonpath='{.spec.clusterIP}' 2>/dev/null) || true + if [ -n "$svc_ip" ]; then + local svc_dns="documentdb-service-${cluster}.${ns}.svc.cluster.local" + uri=$(echo "$uri" | sed "s/$svc_ip/$svc_dns/g") + fi + + # Strip replicaSet=rs0 (incompatible with directConnection to the gateway). + uri=$(echo "$uri" | sed -E 's/[?&]replicaSet=[^&]*//g') + + echo "$uri" +} + +# Extract the gateway port from a connection string (the port after @host:). +# Args: Defaults to 10260 if it cannot be parsed. +gateway_port() { + local uri="$1" port + port=$(echo "$uri" | sed -E 's#.*@[^:/]+:([0-9]+).*#\1#') + echo "${port:-10260}" +} + +# Rewrite a connection string's host:port to point at a local port-forward. +# Args: +to_local_uri() { + local uri="$1" local_port="$2" + echo "$uri" | sed -E "s#@[^:/]+:[0-9]+#@localhost:${local_port}#" +} + +# Start a kubectl port-forward to the DocumentDB gateway service in the +# background and wait until it is ready. Echoes the background PID on stdout. +# Args: +start_port_forward() { + local ns="$1" cluster="$2" local_port="$3" gw_port="$4" logfile="$5" + local svc="documentdb-service-${cluster}" + + kubectl port-forward "svc/$svc" "${local_port}:${gw_port}" \ + -n "$ns" >"$logfile" 2>&1 & + local pid=$! + + local i + for i in $(seq 1 15); do + if grep -q "Forwarding from" "$logfile" 2>/dev/null; then + break + fi + if ! kill -0 "$pid" 2>/dev/null; then + echo "port-forward to $svc failed; see $logfile" >&2 + return 1 + fi + sleep 1 + done + + echo "$pid" +} diff --git a/documentdb-playground/mongoose/scripts/run-app.sh b/documentdb-playground/mongoose/scripts/run-app.sh new file mode 100755 index 00000000..f2a84d35 --- /dev/null +++ b/documentdb-playground/mongoose/scripts/run-app.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Run the Mongoose demo app LOCALLY against a DocumentDB instance running in a +# cluster (local kind or remote AKS). This is the primary path: no image build, +# no in-cluster deployment, just a local Node.js process reaching DocumentDB +# through a temporary kubectl port-forward. +# +# Prerequisites: kubectl, node, npm, and a deployed DocumentDB instance that +# your current kubectl context can reach. +set -euo pipefail + +command -v kubectl >/dev/null || { echo "kubectl is required" >&2; exit 1; } +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" +DOCUMENTDB_NAMESPACE="${DOCUMENTDB_NAMESPACE:-documentdb-test}" +DOCUMENTDB_CLUSTER="${DOCUMENTDB_CLUSTER:-documentdb-cluster}" +LOCAL_PORT="${LOCAL_PORT:-10260}" +PORT="${PORT:-3000}" + +# Resolve the in-cluster connection string, then point it at the local +# port-forward endpoint. +IN_CLUSTER_URI=$(resolve_mongo_uri "$DOCUMENTDB_NAMESPACE" "$DOCUMENTDB_CLUSTER") +GATEWAY_PORT=$(gateway_port "$IN_CLUSTER_URI") +LOCAL_URI=$(to_local_uri "$IN_CLUSTER_URI" "$LOCAL_PORT") + +echo "Starting port-forward to documentdb-service-${DOCUMENTDB_CLUSTER}:${GATEWAY_PORT} -> localhost:${LOCAL_PORT} ..." +PF_PID=$(start_port_forward "$DOCUMENTDB_NAMESPACE" "$DOCUMENTDB_CLUSTER" \ + "$LOCAL_PORT" "$GATEWAY_PORT" /tmp/mongoose-pf-app.log) +trap 'kill "$PF_PID" 2>/dev/null || true' EXIT + +echo "Installing app dependencies (express, mongoose)..." +(cd "$APP_DIR" && npm install --omit=dev --no-audit --no-fund >/dev/null) + +echo "" +echo "=== Mongoose demo app 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 (port-forward is cleaned up automatically)." +echo "" + +MONGO_URI="$LOCAL_URI" PORT="$PORT" node "$APP_DIR/server.js" \ No newline at end of file diff --git a/documentdb-playground/mongoose/scripts/run-test.sh b/documentdb-playground/mongoose/scripts/run-test.sh new file mode 100755 index 00000000..505b2d7f --- /dev/null +++ b/documentdb-playground/mongoose/scripts/run-test.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Run the Mongoose CRUD/compatibility test suite against the deployed +# DocumentDB instance. The test runs locally (Node.js) and reaches DocumentDB +# through a temporary kubectl port-forward. +# +# Prerequisites: kubectl, node, npm, a deployed DocumentDB instance. +set -euo pipefail + +command -v kubectl >/dev/null || { echo "kubectl is required" >&2; exit 1; } +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" +DOCUMENTDB_NAMESPACE="${DOCUMENTDB_NAMESPACE:-documentdb-test}" +DOCUMENTDB_CLUSTER="${DOCUMENTDB_CLUSTER:-documentdb-cluster}" +LOCAL_PORT="${LOCAL_PORT:-10260}" + +# Resolve the in-cluster URI, then rewrite the host:port to the local +# port-forward endpoint. tlsAllowInvalidCertificates lets the self-signed cert +# pass even though the hostname is now localhost. +IN_CLUSTER_URI=$(resolve_mongo_uri "$DOCUMENTDB_NAMESPACE" "$DOCUMENTDB_CLUSTER") +GATEWAY_PORT=$(gateway_port "$IN_CLUSTER_URI") +LOCAL_URI=$(to_local_uri "$IN_CLUSTER_URI" "$LOCAL_PORT") + +echo "Starting port-forward to documentdb-service-${DOCUMENTDB_CLUSTER}:${GATEWAY_PORT} -> localhost:${LOCAL_PORT} ..." +PF_PID=$(start_port_forward "$DOCUMENTDB_NAMESPACE" "$DOCUMENTDB_CLUSTER" \ + "$LOCAL_PORT" "$GATEWAY_PORT" /tmp/mongoose-pf-test.log) +trap 'kill "$PF_PID" 2>/dev/null || true' EXIT + +echo "Installing test dependencies (mongoose)..." +(cd "$APP_DIR" && npm install --omit=dev --no-audit --no-fund >/dev/null) + +echo "" +MONGO_URI="$LOCAL_URI" node "$APP_DIR/mongoose-crud-test.js"