Thin MongoDB ODM built for Standard Schema
mongodb zod deno

Compare changes

Choose any two refs to compare.

.DS_Store

This is a binary file and will not be displayed.

+16
.github/workflows/publish.yml
···
···
+
name: Publish
+
on:
+
push:
+
branches:
+
- main
+
+
jobs:
+
publish:
+
runs-on: ubuntu-latest
+
permissions:
+
contents: read
+
id-token: write
+
steps:
+
- uses: actions/checkout@v4
+
- name: Publish package
+
run: npx jsr publish
+1 -1
.gitignore
···
-
node_modules/
···
+
.DS_Store
-1
.npmignore
···
-
node_modules/
···
+3
.vscode/settings.json
···
···
+
{
+
"git.enabled": false
+
}
+238 -76
README.md
···
-
# mizzleORM
-
A lightweight, fully type-safe MongoDB ORM in TypeScript, inspired by Drizzle ORM.
-
## Features
-
* **Schema-first:** Define and validate document schemas using Zod.
-
* **Type-safe queries:** Auto-complete and type-safe insert/find/update/delete operations.
-
* **Lightweight & modular:** No decorators, no runtime magic โ€“ everything is composable and transparent.
-
* **Developer-first DX:** Simple, minimal API with great IDE support.
-
* Works directly on top of MongoDB's native driver.
-
## Installation
```bash
-
npm install mizzleorm mongodb zod
-
# or
-
yarn add mizzleorm mongodb zod
```
-
## Usage
-
### 1. Define your schema
-
```typescript
// src/schemas/user.ts
-
import { z } from 'zod';
-
import { defineModel } from 'mizzleorm';
-
export const userSchema = defineModel(z.object({
name: z.string(),
-
email: z.string().email(),
age: z.number().int().positive().optional(),
createdAt: z.date().default(() => new Date()),
-
}));
export type User = z.infer<typeof userSchema>;
```
-
### 2. Connect to MongoDB and create a model
-
```typescript
-
// src/index.ts or your main application file
-
import { connect, MongoModel, InferModel, InsertType } from 'mizzleorm';
-
import { userSchema } from './schemas/user'; // Assuming you saved the schema above
-
import { ObjectId } from 'mongodb';
-
// Infer types
type User = InferModel<typeof userSchema>;
-
type UserInsert = InsertType<typeof userSchema>;
async function main() {
-
await connect('mongodb://localhost:27017', 'your_database_name');
-
const UserModel = new MongoModel('users', userSchema);
-
// ... perform operations
await disconnect();
}
···
main().catch(console.error);
```
### 3. Perform operations
-
```typescript
-
// Insert a document
const newUser: UserInsert = {
-
name: 'John Doe',
-
email: 'john.doe@example.com',
age: 30,
};
const insertResult = await UserModel.insertOne(newUser);
-
console.log('Inserted user:', insertResult.insertedId);
-
// Find documents
-
const users = await UserModel.find({ name: 'John Doe' });
-
console.log('Found users:', users);
-
// Find one document
-
const foundUser = await UserModel.findOne({ _id: new ObjectId(insertResult.insertedId) });
-
console.log('Found one user:', foundUser);
-
// Update a document
-
const updateResult = await UserModel.update(
-
{ _id: new ObjectId(insertResult.insertedId) },
-
{ age: 31 }
);
-
console.log('Updated user count:', updateResult.modifiedCount);
-
// Delete documents
-
const deleteResult = await UserModel.delete({ name: 'John Doe' });
-
console.log('Deleted user count:', deleteResult.deletedCount);
```
-
## Project Structure
-
```
-
mongo-orm/
-
โ”œโ”€โ”€ src/
-
โ”‚ โ”œโ”€โ”€ schema.ts # schema definition utility
-
โ”‚ โ”œโ”€โ”€ model.ts # MongoModel wrapper
-
โ”‚ โ”œโ”€โ”€ client.ts # MongoDB connection
-
โ”‚ โ”œโ”€โ”€ index.ts # public API export
-
โ”œโ”€โ”€ examples/
-
โ”‚ โ””โ”€โ”€ user.ts # usage example
-
โ”œโ”€โ”€ tests/
-
โ”œโ”€โ”€ package.json
-
โ”œโ”€โ”€ tsconfig.json
-
โ”œโ”€โ”€ README.md
-
```
-
## Development
-
To build the project:
-
```bash
-
npm run build
-
```
-
To run the example:
-
```bash
-
npm run example
-
```
-
## License
-
MIT
···
+
# **Nozzle**
+
A lightweight, type-safe ODM for MongoDB in TypeScript
+
> **Note:** Nozzle requires MongoDB **4.2 or newer** and works best with the
+
> latest stable MongoDB server (6.x or newer) and the official
+
> [mongodb](https://www.npmjs.com/package/mongodb) Node.js driver (v6+).
+
## โœจ Features
+
- **Schema-first:** Define and validate collections using
+
[Zod](https://zod.dev).
+
- **Type-safe operations:** Auto-complete and strict typings for `insert`,
+
`find`, `update`, and `delete`.
+
- **Minimal & modular:** No decorators or magic. Just clean, composable APIs.
+
- **Built on MongoDB native driver:** Zero overhead with full control.
+
+
---
+
+
## ๐Ÿ“ฆ Installation
```bash
+
deno add jsr:@nozzle/nozzle
```
+
> If you need to upgrade your local MongoDB server, see:
+
> https://www.mongodb.com/docs/manual/administration/install-community/
+
---
+
## ๐Ÿš€ Quick Start
+
+
### 1. Define a schema
+
+
```ts
// src/schemas/user.ts
+
import { z } from "@zod/zod";
+
export const userSchema = z.object({
name: z.string(),
+
email: z.email(),
age: z.number().int().positive().optional(),
createdAt: z.date().default(() => new Date()),
+
});
export type User = z.infer<typeof userSchema>;
```
+
---
+
+
### 2. Initialize connection and model
+
```ts
+
// src/index.ts
+
import { connect, disconnect, InferModel, Input, Model } from "@nozzle/nozzle";
+
import { userSchema } from "./schemas/user";
+
import { ObjectId } from "mongodb"; // v6+ driver recommended
type User = InferModel<typeof userSchema>;
+
type UserInsert = Input<typeof userSchema>;
async function main() {
+
// Basic connection
+
await connect("mongodb://localhost:27017", "your_database_name");
+
+
// Or with connection pooling options
+
await connect("mongodb://localhost:27017", "your_database_name", {
+
maxPoolSize: 10, // Maximum connections in pool
+
minPoolSize: 2, // Minimum connections in pool
+
maxIdleTimeMS: 30000, // Close idle connections after 30s
+
connectTimeoutMS: 10000, // Connection timeout
+
socketTimeoutMS: 45000, // Socket timeout
+
});
+
+
// Production-ready connection with retry logic and resilience
+
await connect("mongodb://localhost:27017", "your_database_name", {
+
// Connection pooling
+
maxPoolSize: 10,
+
minPoolSize: 2,
+
+
// Automatic retry logic (enabled by default)
+
retryReads: true, // Retry failed read operations
+
retryWrites: true, // Retry failed write operations
+
+
// Timeouts
+
connectTimeoutMS: 10000, // Initial connection timeout
+
socketTimeoutMS: 45000, // Socket operation timeout
+
serverSelectionTimeoutMS: 10000, // Server selection timeout
+
+
// Connection resilience
+
maxIdleTimeMS: 30000, // Close idle connections
+
heartbeatFrequencyMS: 10000, // Server health check interval
+
});
+
+
const UserModel = new Model("users", userSchema);
+
// Your operations go here
await disconnect();
}
···
main().catch(console.error);
```
+
---
+
### 3. Perform operations
+
```ts
+
// Insert one
+
// Note: createdAt has a default, so it's optional in the input type
const newUser: UserInsert = {
+
name: "John Doe",
+
email: "john.doe@example.com",
age: 30,
+
// createdAt is optional because of z.date().default(() => new Date())
};
const insertResult = await UserModel.insertOne(newUser);
+
// Find many
+
const users = await UserModel.find({ name: "John Doe" });
+
+
// Find one
+
const found = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
}); // ObjectId from mongodb v6+
+
+
// Update
+
await UserModel.update({ name: "John Doe" }, { age: 31 });
+
+
// Delete
+
await UserModel.delete({ name: "John Doe" });
+
+
// Insert many
+
await UserModel.insertMany([
+
{ name: "Alice", email: "alice@example.com", age: 25 },
+
{ name: "Bob", email: "bob@example.com" },
+
]);
+
+
// Find by ID
+
await UserModel.findById(insertResult.insertedId);
+
// Update one
+
await UserModel.updateOne({ name: "Alice" }, { age: 26 });
+
+
// Replace one
+
await UserModel.replaceOne({ name: "Bob" }, {
+
name: "Bob",
+
email: "bob@newmail.com",
+
age: 22,
+
});
+
// Delete one
+
await UserModel.deleteOne({ name: "Alice" });
+
+
// Count
+
const count = await UserModel.count({ age: { $gte: 18 } });
+
+
// Aggregation
+
const aggregation = await UserModel.aggregate([
+
{ $match: { age: { $gte: 18 } } },
+
{ $group: { _id: null, avgAge: { $avg: "$age" } } },
+
]);
+
+
// Paginated query
+
const paginated = await UserModel.findPaginated(
+
{ age: { $gte: 18 } },
+
{ skip: 0, limit: 10, sort: { age: -1 } },
);
+
+
// Index Management
+
// Create a unique index
+
await UserModel.createIndex({ email: 1 }, { unique: true });
+
+
// Create a compound index
+
await UserModel.createIndex({ name: 1, age: -1 });
+
+
// Create multiple indexes at once
+
await UserModel.createIndexes([
+
{ key: { email: 1 }, name: "email_idx", unique: true },
+
{ key: { name: 1, age: -1 }, name: "name_age_idx" },
+
]);
+
+
// List all indexes
+
const indexes = await UserModel.listIndexes();
+
console.log("Indexes:", indexes);
+
+
// Check if index exists
+
const exists = await UserModel.indexExists("email_idx");
+
+
// Drop an index
+
await UserModel.dropIndex("email_idx");
+
+
// Sync indexes (useful for migrations - creates missing, updates changed)
+
await UserModel.syncIndexes([
+
{ key: { email: 1 }, name: "email_idx", unique: true },
+
{ key: { createdAt: 1 }, name: "created_at_idx" },
+
]);
+
+
// Transactions (Requires MongoDB Replica Set or Sharded Cluster)
+
import { withTransaction } from "@nozzle/nozzle";
+
+
// Automatic transaction management with withTransaction
+
const result = await withTransaction(async (session) => {
+
// All operations in this callback are part of the same transaction
+
const user = await UserModel.insertOne(
+
{ name: "Alice", email: "alice@example.com" },
+
{ session }, // Pass session to each operation
+
);
+
+
const order = await OrderModel.insertOne(
+
{ userId: user.insertedId, total: 100 },
+
{ session },
+
);
+
+
// If any operation fails, the entire transaction is automatically aborted
+
// If callback succeeds, transaction is automatically committed
+
return { user, order };
+
});
+
+
// Manual session management (for advanced use cases)
+
import { endSession, startSession } from "@nozzle/nozzle";
+
+
const session = startSession();
+
try {
+
await session.withTransaction(async () => {
+
await UserModel.insertOne({ name: "Bob", email: "bob@example.com" }, {
+
session,
+
});
+
await UserModel.updateOne({ name: "Alice" }, { balance: 50 }, { session });
+
});
+
} finally {
+
await endSession(session);
+
}
+
// Error Handling
+
import { ConnectionError, ValidationError } from "@nozzle/nozzle";
+
+
try {
+
await UserModel.insertOne({ name: "", email: "invalid" });
+
} catch (error) {
+
if (error instanceof ValidationError) {
+
console.error("Validation failed:", error.operation);
+
// Get field-specific errors
+
const fieldErrors = error.getFieldErrors();
+
console.error("Field errors:", fieldErrors);
+
// { name: ['String must contain at least 1 character(s)'], email: ['Invalid email'] }
+
} else if (error instanceof ConnectionError) {
+
console.error("Connection failed:", error.uri);
+
} else {
+
console.error("Unexpected error:", error);
+
}
+
}
```
+
---
+
## ๐Ÿ—บ๏ธ Roadmap
+
### ๐Ÿ”ด Critical (Must Have)
+
- [x] Transactions support
+
- [x] Connection retry logic
+
- [x] Improved error handling
+
- [x] Connection health checks
+
- [x] Connection pooling configuration
+
### ๐ŸŸก Important (Should Have)
+
+
- [x] Index management
+
- [ ] Middleware/hooks system
+
- [ ] Relationship/population support
+
- [x] Better default value handling
+
- [ ] Comprehensive edge case testing
+
+
### ๐ŸŸข Nice to Have
+
- [x] Pagination support
+
- [ ] Plugin system
+
- [ ] Query builder API
+
- [ ] Virtual fields
+
- [ ] Document/static methods
+
For detailed production readiness assessment, see
+
[PRODUCTION_READINESS_ASSESSMENT.md](./PRODUCTION_READINESS_ASSESSMENT.md).
+
---
+
## ๐Ÿ“„ License
+
MIT โ€” use it freely and contribute back if you'd like!
+
---
+124
bench/ops_bench.ts
···
···
+
import { z } from "@zod/zod";
+
import { MongoMemoryServer } from "mongodb-memory-server-core";
+
import mongoose from "mongoose";
+
import { connect, disconnect, Model } from "../mod.ts";
+
+
/**
+
* Benchmark basic CRUD operations for Nozzle vs Mongoose.
+
*
+
* Run with:
+
* deno bench -A bench/nozzle_vs_mongoose.bench.ts
+
*/
+
+
const userSchema = z.object({
+
name: z.string(),
+
email: z.string().email(),
+
age: z.number().int().positive().optional(),
+
createdAt: z.date().default(() => new Date()),
+
});
+
+
const mongoServer = await MongoMemoryServer.create();
+
const uri = mongoServer.getUri();
+
+
// Use separate DBs to avoid any cross-driver interference
+
const nozzleDbName = "bench_nozzle";
+
const mongooseDbName = "bench_mongoose";
+
+
await connect(uri, nozzleDbName);
+
const NozzleUser = new Model("bench_users_nozzle", userSchema);
+
+
const mongooseConn = await mongoose.connect(uri, { dbName: mongooseDbName });
+
const mongooseUserSchema = new mongoose.Schema(
+
{
+
name: String,
+
email: String,
+
age: Number,
+
createdAt: { type: Date, default: Date.now },
+
},
+
{ collection: "bench_users_mongoose" },
+
);
+
const MongooseUser = mongooseConn.models.BenchUser ||
+
mongooseConn.model("BenchUser", mongooseUserSchema);
+
+
// Start from a clean state
+
await NozzleUser.delete({});
+
await MongooseUser.deleteMany({});
+
+
// Seed base documents for read/update benches
+
const nozzleSeed = await NozzleUser.insertOne({
+
name: "Seed Nozzle",
+
email: "seed-nozzle@example.com",
+
age: 30,
+
});
+
const mongooseSeed = await MongooseUser.create({
+
name: "Seed Mongoose",
+
email: "seed-mongoose@example.com",
+
age: 30,
+
});
+
+
const nozzleSeedId = nozzleSeed.insertedId;
+
const mongooseSeedId = mongooseSeed._id;
+
+
let counter = 0;
+
const nextEmail = (prefix: string) => `${prefix}-${counter++}@bench.dev`;
+
+
Deno.bench("mongoose insertOne", { group: "insertOne" }, async () => {
+
await MongooseUser.insertOne({
+
name: "Mongoose User",
+
email: nextEmail("mongoose"),
+
age: 25,
+
});
+
});
+
+
Deno.bench(
+
"nozzle insertOne",
+
{ group: "insertOne", baseline: true },
+
async () => {
+
await NozzleUser.insertOne({
+
name: "Nozzle User",
+
email: nextEmail("nozzle"),
+
age: 25,
+
});
+
},
+
);
+
+
Deno.bench("mongoose findById", { group: "findById" }, async () => {
+
await MongooseUser.findById(mongooseSeedId);
+
});
+
+
Deno.bench(
+
"nozzle findById",
+
{ group: "findById", baseline: true },
+
async () => {
+
await NozzleUser.findById(nozzleSeedId);
+
},
+
);
+
+
Deno.bench("mongoose updateOne", { group: "updateOne" }, async () => {
+
await MongooseUser.updateOne(
+
{ _id: mongooseSeedId },
+
{ $set: { age: 31 } },
+
);
+
});
+
+
Deno.bench(
+
"nozzle updateOne",
+
{ group: "updateOne", baseline: true },
+
async () => {
+
await NozzleUser.updateOne(
+
{ _id: nozzleSeedId },
+
{ age: 31 },
+
);
+
},
+
);
+
+
// Attempt graceful shutdown when the process exits
+
async function cleanup() {
+
await disconnect();
+
await mongooseConn.disconnect();
+
await mongoServer.stop();
+
}
+
+
globalThis.addEventListener("unload", () => {
+
void cleanup();
+
});
+139
bench/results.json
···
···
+
{
+
"version": 1,
+
"runtime": "Deno/2.5.6 aarch64-apple-darwin",
+
"cpu": "Apple M2 Pro",
+
"benches": [
+
{
+
"origin": "file:///Users/knotbin/Developer/nozzle/bench/ops_bench.ts",
+
"group": "insertOne",
+
"name": "mongoose insertOne",
+
"baseline": false,
+
"results": [
+
{
+
"ok": {
+
"n": 3733,
+
"min": 85750.0,
+
"max": 495459.0,
+
"avg": 134257.0,
+
"p75": 128917.0,
+
"p99": 313291.0,
+
"p995": 344708.0,
+
"p999": 446833.0,
+
"highPrecision": true,
+
"usedExplicitTimers": false
+
}
+
}
+
]
+
},
+
{
+
"origin": "file:///Users/knotbin/Developer/nozzle/bench/ops_bench.ts",
+
"group": "insertOne",
+
"name": "nozzle insertOne",
+
"baseline": true,
+
"results": [
+
{
+
"ok": {
+
"n": 6354,
+
"min": 52667.0,
+
"max": 453875.0,
+
"avg": 78809.0,
+
"p75": 81417.0,
+
"p99": 149417.0,
+
"p995": 201459.0,
+
"p999": 274750.0,
+
"highPrecision": true,
+
"usedExplicitTimers": false
+
}
+
}
+
]
+
},
+
{
+
"origin": "file:///Users/knotbin/Developer/nozzle/bench/ops_bench.ts",
+
"group": "findById",
+
"name": "mongoose findById",
+
"baseline": false,
+
"results": [
+
{
+
"ok": {
+
"n": 3707,
+
"min": 113875.0,
+
"max": 510125.0,
+
"avg": 135223.0,
+
"p75": 137167.0,
+
"p99": 263958.0,
+
"p995": 347458.0,
+
"p999": 428500.0,
+
"highPrecision": true,
+
"usedExplicitTimers": false
+
}
+
}
+
]
+
},
+
{
+
"origin": "file:///Users/knotbin/Developer/nozzle/bench/ops_bench.ts",
+
"group": "findById",
+
"name": "nozzle findById",
+
"baseline": true,
+
"results": [
+
{
+
"ok": {
+
"n": 6045,
+
"min": 70750.0,
+
"max": 1008792.0,
+
"avg": 82859.0,
+
"p75": 83750.0,
+
"p99": 132250.0,
+
"p995": 183500.0,
+
"p999": 311833.0,
+
"highPrecision": true,
+
"usedExplicitTimers": false
+
}
+
}
+
]
+
},
+
{
+
"origin": "file:///Users/knotbin/Developer/nozzle/bench/ops_bench.ts",
+
"group": "updateOne",
+
"name": "mongoose updateOne",
+
"baseline": false,
+
"results": [
+
{
+
"ok": {
+
"n": 4123,
+
"min": 98500.0,
+
"max": 717334.0,
+
"avg": 121572.0,
+
"p75": 123292.0,
+
"p99": 179375.0,
+
"p995": 281417.0,
+
"p999": 342625.0,
+
"highPrecision": true,
+
"usedExplicitTimers": false
+
}
+
}
+
]
+
},
+
{
+
"origin": "file:///Users/knotbin/Developer/nozzle/bench/ops_bench.ts",
+
"group": "updateOne",
+
"name": "nozzle updateOne",
+
"baseline": true,
+
"results": [
+
{
+
"ok": {
+
"n": 6550,
+
"min": 53833.0,
+
"max": 401667.0,
+
"avg": 76456.0,
+
"p75": 76834.0,
+
"p99": 118292.0,
+
"p995": 181500.0,
+
"p999": 299958.0,
+
"highPrecision": true,
+
"usedExplicitTimers": false
+
}
+
}
+
]
+
}
+
]
+
}
+128
client/connection.ts
···
···
+
import { type Db, MongoClient, type MongoClientOptions } from "mongodb";
+
import { ConnectionError } from "../errors.ts";
+
+
/**
+
* Connection management module
+
*
+
* Handles MongoDB connection lifecycle including connect, disconnect,
+
* and connection state management.
+
*/
+
+
export interface Connection {
+
client: MongoClient;
+
db: Db;
+
}
+
+
export interface ConnectOptions extends MongoClientOptions {}
+
+
// Singleton connection state
+
let connection: Connection | null = null;
+
+
/**
+
* Connect to MongoDB with connection pooling, retry logic, and resilience options
+
*
+
* The MongoDB driver handles connection pooling and automatic retries.
+
* Retry logic is enabled by default for both reads and writes in MongoDB 4.2+.
+
*
+
* @param uri - MongoDB connection string
+
* @param dbName - Name of the database to connect to
+
* @param options - Connection options (pooling, retries, timeouts, etc.)
+
* @returns Connection object with client and db
+
*
+
* @example
+
* Basic connection with pooling:
+
* ```ts
+
* await connect("mongodb://localhost:27017", "mydb", {
+
* maxPoolSize: 10,
+
* minPoolSize: 2,
+
* maxIdleTimeMS: 30000,
+
* connectTimeoutMS: 10000,
+
* socketTimeoutMS: 45000,
+
* });
+
* ```
+
*
+
* @example
+
* Production-ready connection with retry logic and resilience:
+
* ```ts
+
* await connect("mongodb://localhost:27017", "mydb", {
+
* // Connection pooling
+
* maxPoolSize: 10,
+
* minPoolSize: 2,
+
*
+
* // Automatic retry logic (enabled by default)
+
* retryReads: true, // Retry failed read operations
+
* retryWrites: true, // Retry failed write operations
+
*
+
* // Timeouts
+
* connectTimeoutMS: 10000, // Initial connection timeout
+
* socketTimeoutMS: 45000, // Socket operation timeout
+
* serverSelectionTimeoutMS: 10000, // Server selection timeout
+
*
+
* // Connection resilience
+
* maxIdleTimeMS: 30000, // Close idle connections
+
* heartbeatFrequencyMS: 10000, // Server health check interval
+
*
+
* // Optional: Compression for reduced bandwidth
+
* compressors: ['snappy', 'zlib'],
+
* });
+
* ```
+
*/
+
export async function connect(
+
uri: string,
+
dbName: string,
+
options?: ConnectOptions,
+
): Promise<Connection> {
+
if (connection) {
+
return connection;
+
}
+
+
try {
+
const client = new MongoClient(uri, options);
+
await client.connect();
+
const db = client.db(dbName);
+
+
connection = { client, db };
+
return connection;
+
} catch (error) {
+
throw new ConnectionError(
+
`Failed to connect to MongoDB: ${
+
error instanceof Error ? error.message : String(error)
+
}`,
+
uri,
+
);
+
}
+
}
+
+
/**
+
* Disconnect from MongoDB and clean up resources
+
*/
+
export async function disconnect(): Promise<void> {
+
if (connection) {
+
await connection.client.close();
+
connection = null;
+
}
+
}
+
+
/**
+
* Get the current database connection
+
*
+
* @returns MongoDB Db instance
+
* @throws {ConnectionError} If not connected
+
* @internal
+
*/
+
export function getDb(): Db {
+
if (!connection) {
+
throw new ConnectionError("MongoDB not connected. Call connect() first.");
+
}
+
return connection.db;
+
}
+
+
/**
+
* Get the current connection state
+
*
+
* @returns Connection object or null if not connected
+
* @internal
+
*/
+
export function getConnection(): Connection | null {
+
return connection;
+
}
+80
client/health.ts
···
···
+
import { getConnection } from "./connection.ts";
+
+
/**
+
* Health check module
+
*
+
* Provides functionality for monitoring MongoDB connection health
+
* including ping operations and response time measurement.
+
*/
+
+
/**
+
* Health check details of the MongoDB connection
+
*
+
* @property healthy - Overall health status of the connection
+
* @property connected - Whether a connection is established
+
* @property responseTimeMs - Response time in milliseconds (if connection is healthy)
+
* @property error - Error message if health check failed
+
* @property timestamp - Timestamp when health check was performed
+
*/
+
export interface HealthCheckResult {
+
healthy: boolean;
+
connected: boolean;
+
responseTimeMs?: number;
+
error?: string;
+
timestamp: Date;
+
}
+
+
/**
+
* Check the health of the MongoDB connection
+
*
+
* Performs a ping operation to verify the database is responsive
+
* and returns detailed health information including response time.
+
*
+
* @returns Health check result with status and metrics
+
*
+
* @example
+
* ```ts
+
* const health = await healthCheck();
+
* if (health.healthy) {
+
* console.log(`Database healthy (${health.responseTimeMs}ms)`);
+
* } else {
+
* console.error(`Database unhealthy: ${health.error}`);
+
* }
+
* ```
+
*/
+
export async function healthCheck(): Promise<HealthCheckResult> {
+
const timestamp = new Date();
+
const connection = getConnection();
+
+
// Check if connection exists
+
if (!connection) {
+
return {
+
healthy: false,
+
connected: false,
+
error: "No active connection. Call connect() first.",
+
timestamp,
+
};
+
}
+
+
try {
+
// Measure ping response time
+
const startTime = performance.now();
+
await connection.db.admin().ping();
+
const endTime = performance.now();
+
const responseTimeMs = Math.round(endTime - startTime);
+
+
return {
+
healthy: true,
+
connected: true,
+
responseTimeMs,
+
timestamp,
+
};
+
} catch (error) {
+
return {
+
healthy: false,
+
connected: true,
+
error: error instanceof Error ? error.message : String(error),
+
timestamp,
+
};
+
}
+
}
+23
client/index.ts
···
···
+
/**
+
* Client module - MongoDB connection and session management
+
*
+
* This module provides all client-level functionality including:
+
* - Connection management (connect, disconnect)
+
* - Health monitoring (healthCheck)
+
* - Transaction support (startSession, endSession, withTransaction)
+
*/
+
+
// Re-export connection management
+
export {
+
connect,
+
type Connection,
+
type ConnectOptions,
+
disconnect,
+
getDb,
+
} from "./connection.ts";
+
+
// Re-export health monitoring
+
export { healthCheck, type HealthCheckResult } from "./health.ts";
+
+
// Re-export transaction management
+
export { endSession, startSession, withTransaction } from "./transactions.ts";
+83
client/transactions.ts
···
···
+
import type { ClientSession, TransactionOptions } from "mongodb";
+
import { getConnection } from "./connection.ts";
+
import { ConnectionError } from "../errors.ts";
+
+
/**
+
* Transaction management module
+
*
+
* Provides session and transaction management functionality including
+
* automatic transaction handling and manual session control.
+
*/
+
+
/**
+
* Start a new client session for transactions
+
*
+
* Sessions must be ended when done using `endSession()`
+
*
+
* @returns New MongoDB ClientSession
+
* @throws {ConnectionError} If not connected
+
*
+
* @example
+
* ```ts
+
* const session = startSession();
+
* try {
+
* // use session
+
* } finally {
+
* await endSession(session);
+
* }
+
* ```
+
*/
+
export function startSession(): ClientSession {
+
const connection = getConnection();
+
if (!connection) {
+
throw new ConnectionError("MongoDB not connected. Call connect() first.");
+
}
+
return connection.client.startSession();
+
}
+
+
/**
+
* End a client session
+
*
+
* @param session - The session to end
+
*/
+
export async function endSession(session: ClientSession): Promise<void> {
+
await session.endSession();
+
}
+
+
/**
+
* Execute a function within a transaction
+
*
+
* Automatically handles session creation, transaction start/commit/abort, and cleanup.
+
* If the callback throws an error, the transaction is automatically aborted.
+
*
+
* @param callback - Async function to execute within the transaction. Receives the session as parameter.
+
* @param options - Optional transaction options (read/write concern, etc.)
+
* @returns The result from the callback function
+
*
+
* @example
+
* ```ts
+
* const result = await withTransaction(async (session) => {
+
* await UserModel.insertOne({ name: "Alice" }, { session });
+
* await OrderModel.insertOne({ userId: "123", total: 100 }, { session });
+
* return { success: true };
+
* });
+
* ```
+
*/
+
export async function withTransaction<T>(
+
callback: (session: ClientSession) => Promise<T>,
+
options?: TransactionOptions,
+
): Promise<T> {
+
const session = startSession();
+
+
try {
+
let result: T;
+
+
await session.withTransaction(async () => {
+
result = await callback(session);
+
}, options);
+
+
return result!;
+
} finally {
+
await endSession(session);
+
}
+
}
+13
deno.json
···
···
+
{
+
"name": "@nozzle/nozzle",
+
"version": "0.1.0",
+
"exports": "./mod.ts",
+
"license": "MIT",
+
"imports": {
+
"@std/assert": "jsr:@std/assert@^1.0.16",
+
"@zod/zod": "jsr:@zod/zod@^4.1.13",
+
"mongodb": "npm:mongodb@^6.18.0",
+
"mongodb-memory-server-core": "npm:mongodb-memory-server-core@^10.3.0",
+
"mongoose": "npm:mongoose@^8.5.2"
+
}
+
}
+333
deno.lock
···
···
+
{
+
"version": "5",
+
"specifiers": {
+
"jsr:@std/assert@*": "1.0.13",
+
"jsr:@std/assert@^1.0.13": "1.0.13",
+
"jsr:@std/assert@^1.0.16": "1.0.16",
+
"jsr:@std/internal@^1.0.10": "1.0.10",
+
"jsr:@std/internal@^1.0.12": "1.0.12",
+
"jsr:@std/internal@^1.0.6": "1.0.10",
+
"jsr:@std/testing@*": "1.0.15",
+
"jsr:@zod/zod@*": "4.1.13",
+
"jsr:@zod/zod@^4.1.13": "4.1.13",
+
"npm:@types/node@*": "22.15.15",
+
"npm:mongodb-memory-server-core@^10.3.0": "10.3.0",
+
"npm:mongodb@^6.18.0": "6.20.0",
+
"npm:mongoose@^8.5.2": "8.20.1"
+
},
+
"jsr": {
+
"@std/assert@1.0.13": {
+
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
+
"dependencies": [
+
"jsr:@std/internal@^1.0.6"
+
]
+
},
+
"@std/assert@1.0.16": {
+
"integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532",
+
"dependencies": [
+
"jsr:@std/internal@^1.0.12"
+
]
+
},
+
"@std/internal@1.0.10": {
+
"integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7"
+
},
+
"@std/internal@1.0.12": {
+
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
+
},
+
"@std/testing@1.0.15": {
+
"integrity": "a490169f5ccb0f3ae9c94fbc69d2cd43603f2cffb41713a85f99bbb0e3087cbc",
+
"dependencies": [
+
"jsr:@std/assert@^1.0.13",
+
"jsr:@std/internal@^1.0.10"
+
]
+
},
+
"@zod/zod@4.1.13": {
+
"integrity": "fef799152d630583b248645fcac03abedd13e39fd2b752d9466b905d73619bfd"
+
}
+
},
+
"npm": {
+
"@mongodb-js/saslprep@1.3.0": {
+
"integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==",
+
"dependencies": [
+
"sparse-bitfield"
+
]
+
},
+
"@types/node@22.15.15": {
+
"integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==",
+
"dependencies": [
+
"undici-types"
+
]
+
},
+
"@types/webidl-conversions@7.0.3": {
+
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
+
},
+
"@types/whatwg-url@11.0.5": {
+
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
+
"dependencies": [
+
"@types/webidl-conversions"
+
]
+
},
+
"agent-base@7.1.4": {
+
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
+
},
+
"async-mutex@0.5.0": {
+
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
+
"dependencies": [
+
"tslib"
+
]
+
},
+
"b4a@1.7.3": {
+
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="
+
},
+
"bare-events@2.8.2": {
+
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="
+
},
+
"bson@6.10.4": {
+
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="
+
},
+
"buffer-crc32@0.2.13": {
+
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
+
},
+
"camelcase@6.3.0": {
+
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
+
},
+
"commondir@1.0.1": {
+
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="
+
},
+
"debug@4.4.3": {
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+
"dependencies": [
+
"ms"
+
]
+
},
+
"events-universal@1.0.1": {
+
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
+
"dependencies": [
+
"bare-events"
+
]
+
},
+
"fast-fifo@1.3.2": {
+
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
+
},
+
"find-cache-dir@3.3.2": {
+
"integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+
"dependencies": [
+
"commondir",
+
"make-dir",
+
"pkg-dir"
+
]
+
},
+
"find-up@4.1.0": {
+
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+
"dependencies": [
+
"locate-path",
+
"path-exists"
+
]
+
},
+
"follow-redirects@1.15.11": {
+
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
+
},
+
"https-proxy-agent@7.0.6": {
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+
"dependencies": [
+
"agent-base",
+
"debug"
+
]
+
},
+
"kareem@2.6.3": {
+
"integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q=="
+
},
+
"locate-path@5.0.0": {
+
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+
"dependencies": [
+
"p-locate"
+
]
+
},
+
"make-dir@3.1.0": {
+
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+
"dependencies": [
+
"semver@6.3.1"
+
]
+
},
+
"memory-pager@1.5.0": {
+
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
+
},
+
"mongodb-connection-string-url@3.0.2": {
+
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
+
"dependencies": [
+
"@types/whatwg-url",
+
"whatwg-url"
+
]
+
},
+
"mongodb-memory-server-core@10.3.0": {
+
"integrity": "sha512-tp+ZfTBAPqHXjROhAFg6HcVVzXaEhh/iHcbY7QPOIiLwr94OkBFAw4pixyGSfP5wI2SZeEA13lXyRmBAhugWgA==",
+
"dependencies": [
+
"async-mutex",
+
"camelcase",
+
"debug",
+
"find-cache-dir",
+
"follow-redirects",
+
"https-proxy-agent",
+
"mongodb@6.18.0",
+
"new-find-package-json",
+
"semver@7.7.3",
+
"tar-stream",
+
"tslib",
+
"yauzl"
+
]
+
},
+
"mongodb@6.18.0": {
+
"integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==",
+
"dependencies": [
+
"@mongodb-js/saslprep",
+
"bson",
+
"mongodb-connection-string-url"
+
]
+
},
+
"mongodb@6.20.0": {
+
"integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
+
"dependencies": [
+
"@mongodb-js/saslprep",
+
"bson",
+
"mongodb-connection-string-url"
+
]
+
},
+
"mongoose@8.20.1": {
+
"integrity": "sha512-G+n3maddlqkQrP1nXxsI0q20144OSo+pe+HzRRGqaC4yK3FLYKqejqB9cbIi+SX7eoRsnG23LHGYNp8n7mWL2Q==",
+
"dependencies": [
+
"bson",
+
"kareem",
+
"mongodb@6.20.0",
+
"mpath",
+
"mquery",
+
"ms",
+
"sift"
+
]
+
},
+
"mpath@0.9.0": {
+
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="
+
},
+
"mquery@5.0.0": {
+
"integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
+
"dependencies": [
+
"debug"
+
]
+
},
+
"ms@2.1.3": {
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+
},
+
"new-find-package-json@2.0.0": {
+
"integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==",
+
"dependencies": [
+
"debug"
+
]
+
},
+
"p-limit@2.3.0": {
+
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+
"dependencies": [
+
"p-try"
+
]
+
},
+
"p-locate@4.1.0": {
+
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+
"dependencies": [
+
"p-limit"
+
]
+
},
+
"p-try@2.2.0": {
+
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
+
},
+
"path-exists@4.0.0": {
+
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
+
},
+
"pend@1.2.0": {
+
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
+
},
+
"pkg-dir@4.2.0": {
+
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+
"dependencies": [
+
"find-up"
+
]
+
},
+
"punycode@2.3.1": {
+
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
+
},
+
"semver@6.3.1": {
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+
"bin": true
+
},
+
"semver@7.7.3": {
+
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+
"bin": true
+
},
+
"sift@17.1.3": {
+
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="
+
},
+
"sparse-bitfield@3.0.3": {
+
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+
"dependencies": [
+
"memory-pager"
+
]
+
},
+
"streamx@2.23.0": {
+
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
+
"dependencies": [
+
"events-universal",
+
"fast-fifo",
+
"text-decoder"
+
]
+
},
+
"tar-stream@3.1.7": {
+
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+
"dependencies": [
+
"b4a",
+
"fast-fifo",
+
"streamx"
+
]
+
},
+
"text-decoder@1.2.3": {
+
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
+
"dependencies": [
+
"b4a"
+
]
+
},
+
"tr46@5.1.1": {
+
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+
"dependencies": [
+
"punycode"
+
]
+
},
+
"tslib@2.8.1": {
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+
},
+
"undici-types@6.21.0": {
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
+
},
+
"webidl-conversions@7.0.0": {
+
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
+
},
+
"whatwg-url@14.2.0": {
+
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+
"dependencies": [
+
"tr46",
+
"webidl-conversions"
+
]
+
},
+
"yauzl@3.2.0": {
+
"integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==",
+
"dependencies": [
+
"buffer-crc32",
+
"pend"
+
]
+
}
+
},
+
"workspace": {
+
"dependencies": [
+
"jsr:@std/assert@^1.0.16",
+
"jsr:@zod/zod@^4.1.13",
+
"npm:mongodb-memory-server-core@^10.3.0",
+
"npm:mongodb@^6.18.0",
+
"npm:mongoose@^8.5.2"
+
]
+
}
+
}
+135
errors.ts
···
···
+
import type { z } from "@zod/zod";
+
+
// Type for Zod validation issues
+
type ValidationIssue = z.ZodIssue;
+
+
/**
+
* Base error class for all Nozzle errors
+
*/
+
export class NozzleError extends Error {
+
constructor(message: string) {
+
super(message);
+
this.name = this.constructor.name;
+
// Maintains proper stack trace for where error was thrown (only available on V8)
+
if (Error.captureStackTrace) {
+
Error.captureStackTrace(this, this.constructor);
+
}
+
}
+
}
+
+
/**
+
* Validation error with structured issue details
+
* Thrown when data fails schema validation
+
*/
+
export class ValidationError extends NozzleError {
+
public readonly issues: ValidationIssue[];
+
public readonly operation: "insert" | "update" | "replace";
+
+
constructor(
+
issues: ValidationIssue[],
+
operation: "insert" | "update" | "replace",
+
) {
+
const message = ValidationError.formatIssues(issues);
+
super(`Validation failed on ${operation}: ${message}`);
+
this.issues = issues;
+
this.operation = operation;
+
}
+
+
private static formatIssues(issues: ValidationIssue[]): string {
+
return issues.map((issue) => {
+
const path = issue.path.join(".");
+
return `${path || "root"}: ${issue.message}`;
+
}).join("; ");
+
}
+
+
/**
+
* Get validation errors grouped by field
+
*/
+
public getFieldErrors(): Record<string, string[]> {
+
const fieldErrors: Record<string, string[]> = {};
+
for (const issue of this.issues) {
+
const field = issue.path.join(".") || "root";
+
if (!fieldErrors[field]) {
+
fieldErrors[field] = [];
+
}
+
fieldErrors[field].push(issue.message);
+
}
+
return fieldErrors;
+
}
+
}
+
+
/**
+
* Connection error
+
* Thrown when database connection fails or is not established
+
*/
+
export class ConnectionError extends NozzleError {
+
public readonly uri?: string;
+
+
constructor(message: string, uri?: string) {
+
super(message);
+
this.uri = uri;
+
}
+
}
+
+
/**
+
* Configuration error
+
* Thrown when invalid configuration options are provided
+
*/
+
export class ConfigurationError extends NozzleError {
+
public readonly option?: string;
+
+
constructor(message: string, option?: string) {
+
super(message);
+
this.option = option;
+
}
+
}
+
+
/**
+
* Document not found error
+
* Thrown when a required document is not found
+
*/
+
export class DocumentNotFoundError extends NozzleError {
+
public readonly query: unknown;
+
public readonly collection: string;
+
+
constructor(collection: string, query: unknown) {
+
super(`Document not found in collection '${collection}'`);
+
this.collection = collection;
+
this.query = query;
+
}
+
}
+
+
/**
+
* Operation error
+
* Thrown when a database operation fails
+
*/
+
export class OperationError extends NozzleError {
+
public readonly operation: string;
+
public readonly collection?: string;
+
public override readonly cause?: Error;
+
+
constructor(
+
operation: string,
+
message: string,
+
collection?: string,
+
cause?: Error,
+
) {
+
super(`${operation} operation failed: ${message}`);
+
this.operation = operation;
+
this.collection = collection;
+
this.cause = cause;
+
}
+
}
+
+
/**
+
* Async validation not supported error
+
* Thrown when async validation is attempted
+
*/
+
export class AsyncValidationError extends NozzleError {
+
constructor() {
+
super(
+
"Async validation is not currently supported. " +
+
"Please use synchronous validation schemas.",
+
);
+
}
+
}
-115
examples/user.js
···
-
"use strict";
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
-
return new (P || (P = Promise))(function (resolve, reject) {
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
-
});
-
};
-
var __generator = (this && this.__generator) || function (thisArg, body) {
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
-
function verb(n) { return function (v) { return step([n, v]); }; }
-
function step(op) {
-
if (f) throw new TypeError("Generator is already executing.");
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
-
if (y = 0, t) op = [op[0] & 2, t.value];
-
switch (op[0]) {
-
case 0: case 1: t = op; break;
-
case 4: _.label++; return { value: op[1], done: false };
-
case 5: _.label++; y = op[1]; op = [0]; continue;
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
-
default:
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
-
if (t[2]) _.ops.pop();
-
_.trys.pop(); continue;
-
}
-
op = body.call(thisArg, _);
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
-
}
-
};
-
Object.defineProperty(exports, "__esModule", { value: true });
-
var zod_1 = require("zod");
-
var src_1 = require("../src");
-
var mongodb_1 = require("mongodb");
-
// 1. Define your schema using Zod
-
var userSchema = (0, src_1.defineModel)(zod_1.z.object({
-
name: zod_1.z.string(),
-
email: zod_1.z.string().email(),
-
age: zod_1.z.number().int().positive().optional(),
-
createdAt: zod_1.z.date().default(function () { return new Date(); }),
-
}));
-
function runExample() {
-
return __awaiter(this, void 0, void 0, function () {
-
var UserModel, newUser, insertResult, users, foundUser, updateResult, updatedUser, deleteResult, error_1;
-
return __generator(this, function (_a) {
-
switch (_a.label) {
-
case 0:
-
_a.trys.push([0, 9, 10, 12]);
-
// 3. Connect to MongoDB
-
return [4 /*yield*/, (0, src_1.connect)('mongodb://localhost:27017', 'mizzleorm_example')];
-
case 1:
-
// 3. Connect to MongoDB
-
_a.sent();
-
console.log('Connected to MongoDB');
-
UserModel = new src_1.MongoModel('users', userSchema);
-
// Clean up previous data
-
return [4 /*yield*/, UserModel.delete({})];
-
case 2:
-
// Clean up previous data
-
_a.sent();
-
newUser = {
-
name: 'Alice Smith',
-
email: 'alice@example.com',
-
age: 30,
-
};
-
return [4 /*yield*/, UserModel.insertOne(newUser)];
-
case 3:
-
insertResult = _a.sent();
-
console.log('Inserted user:', insertResult.insertedId);
-
return [4 /*yield*/, UserModel.find({ name: 'Alice Smith' })];
-
case 4:
-
users = _a.sent();
-
console.log('Found users:', users);
-
return [4 /*yield*/, UserModel.findOne({ _id: new mongodb_1.ObjectId(insertResult.insertedId) })];
-
case 5:
-
foundUser = _a.sent();
-
console.log('Found one user:', foundUser);
-
return [4 /*yield*/, UserModel.update({ _id: new mongodb_1.ObjectId(insertResult.insertedId) }, { age: 31 })];
-
case 6:
-
updateResult = _a.sent();
-
console.log('Updated user count:', updateResult.modifiedCount);
-
return [4 /*yield*/, UserModel.findOne({ _id: new mongodb_1.ObjectId(insertResult.insertedId) })];
-
case 7:
-
updatedUser = _a.sent();
-
console.log('Updated user data:', updatedUser);
-
return [4 /*yield*/, UserModel.delete({ name: 'Alice Smith' })];
-
case 8:
-
deleteResult = _a.sent();
-
console.log('Deleted user count:', deleteResult.deletedCount);
-
return [3 /*break*/, 12];
-
case 9:
-
error_1 = _a.sent();
-
console.error('Error during example run:', error_1);
-
return [3 /*break*/, 12];
-
case 10:
-
// 9. Disconnect from MongoDB
-
return [4 /*yield*/, (0, src_1.disconnect)()];
-
case 11:
-
// 9. Disconnect from MongoDB
-
_a.sent();
-
console.log('Disconnected from MongoDB');
-
return [7 /*endfinally*/];
-
case 12: return [2 /*return*/];
-
}
-
});
-
});
-
}
-
runExample();
···
-73
examples/user.ts
···
-
import { z } from 'zod';
-
import { defineModel, MongoModel, connect, disconnect, InferModel, InsertType } from '../src';
-
import { ObjectId } from 'mongodb';
-
-
// 1. Define your schema using Zod
-
const userSchema = defineModel(z.object({
-
name: z.string(),
-
email: z.string().email(),
-
age: z.number().int().positive().optional(),
-
createdAt: z.date().default(() => new Date()),
-
}));
-
-
// Infer the TypeScript type from the Zod schema
-
type User = InferModel<typeof userSchema>;
-
type UserInsert = InsertType<typeof userSchema>;
-
-
async function runExample() {
-
try {
-
// 3. Connect to MongoDB
-
await connect('mongodb://localhost:27017', 'mizzleorm_example');
-
console.log('Connected to MongoDB');
-
-
// 2. Create a MongoModel for your collection
-
const UserModel = new MongoModel('users', userSchema);
-
-
// Clean up previous data
-
await UserModel.delete({});
-
-
// 4. Insert a new document
-
const newUser: UserInsert = {
-
name: 'Alice Smith',
-
email: 'alice@example.com',
-
age: 30,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
console.log('Inserted user:', insertResult.insertedId);
-
-
// 5. Find documents
-
const users = await UserModel.find({ name: 'Alice Smith' });
-
console.log('Found users:', users);
-
-
// 6. Find one document
-
const foundUser = await UserModel.findOne({ _id: new ObjectId(insertResult.insertedId) });
-
console.log('Found one user:', foundUser);
-
-
// 7. Update a document
-
const updateResult = await UserModel.update(
-
{ _id: new ObjectId(insertResult.insertedId) },
-
{ age: 31 }
-
);
-
console.log('Updated user count:', updateResult.modifiedCount);
-
-
const updatedUser = await UserModel.findOne({ _id: new ObjectId(insertResult.insertedId) });
-
console.log('Updated user data:', updatedUser);
-
-
// 8. Delete documents
-
const deleteResult = await UserModel.delete({ name: 'Alice Smith' });
-
console.log('Deleted user count:', deleteResult.deletedCount);
-
-
} catch (error) {
-
console.error('Error during example run:', error);
-
} finally {
-
// 9. Disconnect from MongoDB
-
await disconnect();
-
console.log('Disconnected from MongoDB');
-
}
-
}
-
-
runExample();
-
-
-
-
···
+24
mod.ts
···
···
+
export type { Infer, Input, Schema } from "./types.ts";
+
export {
+
connect,
+
type ConnectOptions,
+
disconnect,
+
endSession,
+
healthCheck,
+
type HealthCheckResult,
+
startSession,
+
withTransaction,
+
} from "./client/index.ts";
+
export { Model } from "./model/index.ts";
+
export {
+
AsyncValidationError,
+
ConfigurationError,
+
ConnectionError,
+
DocumentNotFoundError,
+
NozzleError,
+
OperationError,
+
ValidationError,
+
} from "./errors.ts";
+
+
// Re-export MongoDB types that users might need
+
export type { ClientSession, TransactionOptions } from "mongodb";
+369
model/core.ts
···
···
+
import type { z } from "@zod/zod";
+
import type {
+
AggregateOptions,
+
BulkWriteOptions,
+
Collection,
+
CountDocumentsOptions,
+
DeleteOptions,
+
DeleteResult,
+
Document,
+
Filter,
+
FindOneAndReplaceOptions,
+
FindOneAndUpdateOptions,
+
FindOptions,
+
InsertManyResult,
+
InsertOneOptions,
+
InsertOneResult,
+
ModifyResult,
+
OptionalUnlessRequiredId,
+
ReplaceOptions,
+
UpdateFilter,
+
UpdateOptions,
+
UpdateResult,
+
WithId,
+
} from "mongodb";
+
import { ObjectId } from "mongodb";
+
import type { Infer, Input, Schema } from "../types.ts";
+
import {
+
applyDefaultsForUpsert,
+
parse,
+
parsePartial,
+
parseReplace,
+
} from "./validation.ts";
+
+
/**
+
* Core CRUD operations for the Model class
+
*
+
* This module contains all basic create, read, update, and delete operations
+
* with automatic Zod validation and transaction support.
+
*/
+
+
/**
+
* Insert a single document into the collection
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param data - Document data to insert
+
* @param options - Insert options (including session for transactions)
+
* @returns Insert result with insertedId
+
*/
+
export async function insertOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
data: Input<T>,
+
options?: InsertOneOptions,
+
): Promise<InsertOneResult<Infer<T>>> {
+
const validatedData = parse(schema, data);
+
return await collection.insertOne(
+
validatedData as OptionalUnlessRequiredId<Infer<T>>,
+
options,
+
);
+
}
+
+
/**
+
* Insert multiple documents into the collection
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param data - Array of document data to insert
+
* @param options - Insert options (including session for transactions)
+
* @returns Insert result with insertedIds
+
*/
+
export async function insertMany<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
data: Input<T>[],
+
options?: BulkWriteOptions,
+
): Promise<InsertManyResult<Infer<T>>> {
+
const validatedData = data.map((item) => parse(schema, item));
+
return await collection.insertMany(
+
validatedData as OptionalUnlessRequiredId<Infer<T>>[],
+
options,
+
);
+
}
+
+
/**
+
* Find multiple documents matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Find options (including session for transactions)
+
* @returns Array of matching documents
+
*/
+
export async function find<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: FindOptions,
+
): Promise<(WithId<Infer<T>>)[]> {
+
return await collection.find(query, options).toArray();
+
}
+
+
/**
+
* Find a single document matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Find options (including session for transactions)
+
* @returns Matching document or null if not found
+
*/
+
export async function findOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: FindOptions,
+
): Promise<WithId<Infer<T>> | null> {
+
return await collection.findOne(query, options);
+
}
+
+
/**
+
* Find a document by its MongoDB ObjectId
+
*
+
* @param collection - MongoDB collection
+
* @param id - Document ID (string or ObjectId)
+
* @param options - Find options (including session for transactions)
+
* @returns Matching document or null if not found
+
*/
+
export async function findById<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
id: string | ObjectId,
+
options?: FindOptions,
+
): Promise<WithId<Infer<T>> | null> {
+
const objectId = typeof id === "string" ? new ObjectId(id) : id;
+
return await findOne(
+
collection,
+
{ _id: objectId } as Filter<Infer<T>>,
+
options,
+
);
+
}
+
+
/**
+
* Update multiple documents matching the query
+
*
+
* Case handling:
+
* - If upsert: false (or undefined) โ†’ Normal update, no defaults applied
+
* - If upsert: true โ†’ Defaults added to $setOnInsert for new document creation
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param query - MongoDB query filter
+
* @param data - Partial data to update
+
* @param options - Update options (including session for transactions and upsert flag)
+
* @returns Update result
+
*/
+
export async function update<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: UpdateOptions,
+
): Promise<UpdateResult<Infer<T>>> {
+
const validatedData = parsePartial(schema, data);
+
let updateDoc: UpdateFilter<Infer<T>> = {
+
$set: validatedData as Partial<Infer<T>>,
+
};
+
+
// If this is an upsert, apply defaults using $setOnInsert
+
if (options?.upsert) {
+
updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
+
}
+
+
return await collection.updateMany(query, updateDoc, options);
+
}
+
+
/**
+
* Update a single document matching the query
+
*
+
* Case handling:
+
* - If upsert: false (or undefined) โ†’ Normal update, no defaults applied
+
* - If upsert: true โ†’ Defaults added to $setOnInsert for new document creation
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param query - MongoDB query filter
+
* @param data - Partial data to update
+
* @param options - Update options (including session for transactions and upsert flag)
+
* @returns Update result
+
*/
+
export async function updateOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: UpdateOptions,
+
): Promise<UpdateResult<Infer<T>>> {
+
const validatedData = parsePartial(schema, data);
+
let updateDoc: UpdateFilter<Infer<T>> = {
+
$set: validatedData as Partial<Infer<T>>,
+
};
+
+
// If this is an upsert, apply defaults using $setOnInsert
+
if (options?.upsert) {
+
updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
+
}
+
+
return await collection.updateOne(query, updateDoc, options);
+
}
+
+
/**
+
* Replace a single document matching the query
+
*
+
* Case handling:
+
* - If upsert: false (or undefined) โ†’ Normal replace on existing doc, no additional defaults
+
* - If upsert: true โ†’ Defaults applied via parse() since we're passing a full document
+
*
+
* Note: For replace operations, defaults are automatically applied by the schema's
+
* parse() function which treats missing fields as candidates for defaults. This works
+
* for both regular replaces and upsert-creates since we're providing a full document.
+
*
+
* @param collection - MongoDB collection
+
* @param schema - Zod schema for validation
+
* @param query - MongoDB query filter
+
* @param data - Complete document data for replacement
+
* @param options - Replace options (including session for transactions and upsert flag)
+
* @returns Update result
+
*/
+
export async function replaceOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
query: Filter<Infer<T>>,
+
data: Input<T>,
+
options?: ReplaceOptions,
+
): Promise<UpdateResult<Infer<T>>> {
+
// parseReplace will apply all schema defaults to missing fields
+
// This works correctly for both regular replaces and upsert-created documents
+
const validatedData = parseReplace(schema, data);
+
+
// Remove _id from validatedData for replaceOne (it will use the query's _id)
+
const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
+
return await collection.replaceOne(
+
query,
+
withoutId as Infer<T>,
+
options,
+
);
+
}
+
+
/**
+
* Find a single document and update it
+
*
+
* Case handling:
+
* - If upsert: false (or undefined) โ†’ Normal update
+
* - If upsert: true โ†’ Defaults added to $setOnInsert for new document creation
+
*/
+
export async function findOneAndUpdate<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: FindOneAndUpdateOptions,
+
): Promise<ModifyResult<Infer<T>>> {
+
const validatedData = parsePartial(schema, data);
+
let updateDoc: UpdateFilter<Infer<T>> = {
+
$set: validatedData as Partial<Infer<T>>,
+
};
+
+
if (options?.upsert) {
+
updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
+
}
+
+
const resolvedOptions: FindOneAndUpdateOptions & {
+
includeResultMetadata: true;
+
} = {
+
...(options ?? {}),
+
includeResultMetadata: true as const,
+
};
+
+
return await collection.findOneAndUpdate(query, updateDoc, resolvedOptions);
+
}
+
+
/**
+
* Find a single document and replace it
+
*
+
* Defaults are applied via parseReplace(), which fills in missing fields
+
* for both normal replacements and upsert-created documents.
+
*/
+
export async function findOneAndReplace<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
schema: T,
+
query: Filter<Infer<T>>,
+
data: Input<T>,
+
options?: FindOneAndReplaceOptions,
+
): Promise<ModifyResult<Infer<T>>> {
+
const validatedData = parseReplace(schema, data);
+
const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
+
+
const resolvedOptions: FindOneAndReplaceOptions & {
+
includeResultMetadata: true;
+
} = {
+
...(options ?? {}),
+
includeResultMetadata: true as const,
+
};
+
+
return await collection.findOneAndReplace(
+
query,
+
withoutId as Infer<T>,
+
resolvedOptions,
+
);
+
}
+
+
/**
+
* Delete multiple documents matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Delete options (including session for transactions)
+
* @returns Delete result
+
*/
+
export async function deleteMany<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions,
+
): Promise<DeleteResult> {
+
return await collection.deleteMany(query, options);
+
}
+
+
/**
+
* Delete a single document matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Delete options (including session for transactions)
+
* @returns Delete result
+
*/
+
export async function deleteOne<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions,
+
): Promise<DeleteResult> {
+
return await collection.deleteOne(query, options);
+
}
+
+
/**
+
* Count documents matching the query
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Count options (including session for transactions)
+
* @returns Number of matching documents
+
*/
+
export async function count<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options?: CountDocumentsOptions,
+
): Promise<number> {
+
return await collection.countDocuments(query, options);
+
}
+
+
/**
+
* Execute an aggregation pipeline
+
*
+
* @param collection - MongoDB collection
+
* @param pipeline - MongoDB aggregation pipeline
+
* @param options - Aggregate options (including session for transactions)
+
* @returns Array of aggregation results
+
*/
+
export async function aggregate<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
pipeline: Document[],
+
options?: AggregateOptions,
+
): Promise<Document[]> {
+
return await collection.aggregate(pipeline, options).toArray();
+
}
+432
model/index.ts
···
···
+
import type { z } from "@zod/zod";
+
import type {
+
AggregateOptions,
+
BulkWriteOptions,
+
Collection,
+
CountDocumentsOptions,
+
CreateIndexesOptions,
+
DeleteOptions,
+
DeleteResult,
+
Document,
+
DropIndexesOptions,
+
Filter,
+
FindOneAndReplaceOptions,
+
FindOneAndUpdateOptions,
+
FindOptions,
+
IndexDescription,
+
IndexSpecification,
+
InsertManyResult,
+
InsertOneOptions,
+
InsertOneResult,
+
ListIndexesOptions,
+
ModifyResult,
+
ReplaceOptions,
+
UpdateOptions,
+
UpdateResult,
+
WithId,
+
} from "mongodb";
+
import type { ObjectId } from "mongodb";
+
import { getDb } from "../client/connection.ts";
+
import type { Indexes, Infer, Input, ModelDef, Schema } from "../types.ts";
+
import * as core from "./core.ts";
+
import * as indexes from "./indexes.ts";
+
import * as pagination from "./pagination.ts";
+
+
/**
+
* Model class for type-safe MongoDB operations
+
*
+
* Provides a clean API for CRUD operations, pagination, and index management
+
* with automatic Zod validation and TypeScript type safety.
+
*
+
* @example
+
* ```ts
+
* const userSchema = z.object({
+
* name: z.string(),
+
* email: z.string().email(),
+
* });
+
*
+
* const UserModel = new Model("users", userSchema);
+
* await UserModel.insertOne({ name: "Alice", email: "alice@example.com" });
+
* ```
+
*/
+
export class Model<T extends Schema> {
+
private collection: Collection<Infer<T>>;
+
private schema: T;
+
private indexes?: Indexes;
+
+
constructor(collectionName: string, definition: ModelDef<T> | T) {
+
if ("schema" in definition) {
+
this.schema = definition.schema;
+
this.indexes = definition.indexes;
+
} else {
+
this.schema = definition as T;
+
}
+
this.collection = getDb().collection<Infer<T>>(collectionName);
+
+
// Automatically create indexes if they were provided
+
if (this.indexes && this.indexes.length > 0) {
+
// Fire and forget - indexes will be created asynchronously
+
indexes.syncIndexes(this.collection, this.indexes);
+
}
+
}
+
+
// ============================================================================
+
// CRUD Operations (delegated to core.ts)
+
// ============================================================================
+
+
/**
+
* Insert a single document into the collection
+
*
+
* @param data - Document data to insert
+
* @param options - Insert options (including session for transactions)
+
* @returns Insert result with insertedId
+
*/
+
async insertOne(
+
data: Input<T>,
+
options?: InsertOneOptions,
+
): Promise<InsertOneResult<Infer<T>>> {
+
return await core.insertOne(this.collection, this.schema, data, options);
+
}
+
+
/**
+
* Insert multiple documents into the collection
+
*
+
* @param data - Array of document data to insert
+
* @param options - Insert options (including session for transactions)
+
* @returns Insert result with insertedIds
+
*/
+
async insertMany(
+
data: Input<T>[],
+
options?: BulkWriteOptions,
+
): Promise<InsertManyResult<Infer<T>>> {
+
return await core.insertMany(this.collection, this.schema, data, options);
+
}
+
+
/**
+
* Find multiple documents matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Find options (including session for transactions)
+
* @returns Array of matching documents
+
*/
+
async find(
+
query: Filter<Infer<T>>,
+
options?: FindOptions,
+
): Promise<(WithId<Infer<T>>)[]> {
+
return await core.find(this.collection, query, options);
+
}
+
+
/**
+
* Find a single document matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Find options (including session for transactions)
+
* @returns Matching document or null if not found
+
*/
+
async findOne(
+
query: Filter<Infer<T>>,
+
options?: FindOptions,
+
): Promise<WithId<Infer<T>> | null> {
+
return await core.findOne(this.collection, query, options);
+
}
+
+
/**
+
* Find a document by its MongoDB ObjectId
+
*
+
* @param id - Document ID (string or ObjectId)
+
* @param options - Find options (including session for transactions)
+
* @returns Matching document or null if not found
+
*/
+
async findById(
+
id: string | ObjectId,
+
options?: FindOptions,
+
): Promise<WithId<Infer<T>> | null> {
+
return await core.findById(this.collection, id, options);
+
}
+
+
/**
+
* Update multiple documents matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param data - Partial data to update
+
* @param options - Update options (including session for transactions)
+
* @returns Update result
+
*/
+
async update(
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: UpdateOptions,
+
): Promise<UpdateResult<Infer<T>>> {
+
return await core.update(
+
this.collection,
+
this.schema,
+
query,
+
data,
+
options,
+
);
+
}
+
+
/**
+
* Update a single document matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param data - Partial data to update
+
* @param options - Update options (including session for transactions)
+
* @returns Update result
+
*/
+
async updateOne(
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: UpdateOptions,
+
): Promise<UpdateResult<Infer<T>>> {
+
return await core.updateOne(
+
this.collection,
+
this.schema,
+
query,
+
data,
+
options,
+
);
+
}
+
+
/**
+
* Find a single document and update it
+
*
+
* @param query - MongoDB query filter
+
* @param data - Partial data to update
+
* @param options - FindOneAndUpdate options (including upsert and returnDocument)
+
* @returns Modify result containing the matched document
+
*/
+
async findOneAndUpdate(
+
query: Filter<Infer<T>>,
+
data: Partial<z.infer<T>>,
+
options?: FindOneAndUpdateOptions,
+
): Promise<ModifyResult<Infer<T>>> {
+
return await core.findOneAndUpdate(
+
this.collection,
+
this.schema,
+
query,
+
data,
+
options,
+
);
+
}
+
+
/**
+
* Replace a single document matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param data - Complete document data for replacement
+
* @param options - Replace options (including session for transactions)
+
* @returns Update result
+
*/
+
async replaceOne(
+
query: Filter<Infer<T>>,
+
data: Input<T>,
+
options?: ReplaceOptions,
+
): Promise<UpdateResult<Infer<T>>> {
+
return await core.replaceOne(
+
this.collection,
+
this.schema,
+
query,
+
data,
+
options,
+
);
+
}
+
+
/**
+
* Find a single document and replace it
+
*
+
* @param query - MongoDB query filter
+
* @param data - Complete document data for replacement
+
* @param options - FindOneAndReplace options (including upsert and returnDocument)
+
* @returns Modify result containing the matched document
+
*/
+
async findOneAndReplace(
+
query: Filter<Infer<T>>,
+
data: Input<T>,
+
options?: FindOneAndReplaceOptions,
+
): Promise<ModifyResult<Infer<T>>> {
+
return await core.findOneAndReplace(
+
this.collection,
+
this.schema,
+
query,
+
data,
+
options,
+
);
+
}
+
+
/**
+
* Delete multiple documents matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Delete options (including session for transactions)
+
* @returns Delete result
+
*/
+
async delete(
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions,
+
): Promise<DeleteResult> {
+
return await core.deleteMany(this.collection, query, options);
+
}
+
+
/**
+
* Delete a single document matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Delete options (including session for transactions)
+
* @returns Delete result
+
*/
+
async deleteOne(
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions,
+
): Promise<DeleteResult> {
+
return await core.deleteOne(this.collection, query, options);
+
}
+
+
/**
+
* Count documents matching the query
+
*
+
* @param query - MongoDB query filter
+
* @param options - Count options (including session for transactions)
+
* @returns Number of matching documents
+
*/
+
async count(
+
query: Filter<Infer<T>>,
+
options?: CountDocumentsOptions,
+
): Promise<number> {
+
return await core.count(this.collection, query, options);
+
}
+
+
/**
+
* Execute an aggregation pipeline
+
*
+
* @param pipeline - MongoDB aggregation pipeline
+
* @param options - Aggregate options (including session for transactions)
+
* @returns Array of aggregation results
+
*/
+
async aggregate(
+
pipeline: Document[],
+
options?: AggregateOptions,
+
): Promise<Document[]> {
+
return await core.aggregate(this.collection, pipeline, options);
+
}
+
+
// ============================================================================
+
// Pagination (delegated to pagination.ts)
+
// ============================================================================
+
+
/**
+
* Find documents with pagination support
+
*
+
* @param query - MongoDB query filter
+
* @param options - Pagination options (skip, limit, sort)
+
* @returns Array of matching documents
+
*/
+
async findPaginated(
+
query: Filter<Infer<T>>,
+
options: { skip?: number; limit?: number; sort?: Document } = {},
+
): Promise<(WithId<Infer<T>>)[]> {
+
return await pagination.findPaginated(this.collection, query, options);
+
}
+
+
// ============================================================================
+
// Index Management (delegated to indexes.ts)
+
// ============================================================================
+
+
/**
+
* Create a single index on the collection
+
*
+
* @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
+
* @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
+
* @returns The name of the created index
+
*/
+
async createIndex(
+
keys: IndexSpecification,
+
options?: CreateIndexesOptions,
+
): Promise<string> {
+
return await indexes.createIndex(this.collection, keys, options);
+
}
+
+
/**
+
* Create multiple indexes on the collection
+
*
+
* @param indexes - Array of index descriptions
+
* @param options - Index creation options
+
* @returns Array of index names created
+
*/
+
async createIndexes(
+
indexList: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
return await indexes.createIndexes(this.collection, indexList, options);
+
}
+
+
/**
+
* Drop a single index from the collection
+
*
+
* @param index - Index name or specification
+
* @param options - Drop index options
+
*/
+
async dropIndex(
+
index: string | IndexSpecification,
+
options?: DropIndexesOptions,
+
): Promise<void> {
+
return await indexes.dropIndex(this.collection, index, options);
+
}
+
+
/**
+
* Drop all indexes from the collection (except _id index)
+
*
+
* @param options - Drop index options
+
*/
+
async dropIndexes(options?: DropIndexesOptions): Promise<void> {
+
return await indexes.dropIndexes(this.collection, options);
+
}
+
+
/**
+
* List all indexes on the collection
+
*
+
* @param options - List indexes options
+
* @returns Array of index information
+
*/
+
async listIndexes(
+
options?: ListIndexesOptions,
+
): Promise<IndexDescription[]> {
+
return await indexes.listIndexes(this.collection, options);
+
}
+
+
/**
+
* Get index information by name
+
*
+
* @param indexName - Name of the index
+
* @returns Index description or null if not found
+
*/
+
async getIndex(indexName: string): Promise<IndexDescription | null> {
+
return await indexes.getIndex(this.collection, indexName);
+
}
+
+
/**
+
* Check if an index exists
+
*
+
* @param indexName - Name of the index
+
* @returns True if index exists, false otherwise
+
*/
+
async indexExists(indexName: string): Promise<boolean> {
+
return await indexes.indexExists(this.collection, indexName);
+
}
+
+
/**
+
* Synchronize indexes - create indexes if they don't exist, update if they differ
+
*
+
* This is useful for ensuring indexes match your schema definition
+
*
+
* @param indexes - Array of index descriptions to synchronize
+
* @param options - Options for index creation
+
* @returns Array of index names that were created
+
*/
+
async syncIndexes(
+
indexList: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
return await indexes.syncIndexes(this.collection, indexList, options);
+
}
+
}
+180
model/indexes.ts
···
···
+
import type {
+
Collection,
+
CreateIndexesOptions,
+
DropIndexesOptions,
+
IndexDescription,
+
IndexSpecification,
+
ListIndexesOptions,
+
} from "mongodb";
+
import type { Infer, Schema } from "../types.ts";
+
+
/**
+
* Index management operations for the Model class
+
*
+
* This module contains all index-related operations including creation,
+
* deletion, listing, and synchronization of indexes.
+
*/
+
+
/**
+
* Create a single index on the collection
+
*
+
* @param collection - MongoDB collection
+
* @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
+
* @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
+
* @returns The name of the created index
+
*/
+
export async function createIndex<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
keys: IndexSpecification,
+
options?: CreateIndexesOptions,
+
): Promise<string> {
+
return await collection.createIndex(keys, options);
+
}
+
+
/**
+
* Create multiple indexes on the collection
+
*
+
* @param collection - MongoDB collection
+
* @param indexes - Array of index descriptions
+
* @param options - Index creation options
+
* @returns Array of index names created
+
*/
+
export async function createIndexes<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
indexes: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
return await collection.createIndexes(indexes, options);
+
}
+
+
/**
+
* Drop a single index from the collection
+
*
+
* @param collection - MongoDB collection
+
* @param index - Index name or specification
+
* @param options - Drop index options
+
*/
+
export async function dropIndex<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
index: string | IndexSpecification,
+
options?: DropIndexesOptions,
+
): Promise<void> {
+
await collection.dropIndex(index as string, options);
+
}
+
+
/**
+
* Drop all indexes from the collection (except _id index)
+
*
+
* @param collection - MongoDB collection
+
* @param options - Drop index options
+
*/
+
export async function dropIndexes<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
options?: DropIndexesOptions,
+
): Promise<void> {
+
await collection.dropIndexes(options);
+
}
+
+
/**
+
* List all indexes on the collection
+
*
+
* @param collection - MongoDB collection
+
* @param options - List indexes options
+
* @returns Array of index information
+
*/
+
export async function listIndexes<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
options?: ListIndexesOptions,
+
): Promise<IndexDescription[]> {
+
const indexes = await collection.listIndexes(options).toArray();
+
return indexes as IndexDescription[];
+
}
+
+
/**
+
* Get index information by name
+
*
+
* @param collection - MongoDB collection
+
* @param indexName - Name of the index
+
* @returns Index description or null if not found
+
*/
+
export async function getIndex<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
indexName: string,
+
): Promise<IndexDescription | null> {
+
const indexes = await listIndexes(collection);
+
return indexes.find((idx) => idx.name === indexName) || null;
+
}
+
+
/**
+
* Check if an index exists
+
*
+
* @param collection - MongoDB collection
+
* @param indexName - Name of the index
+
* @returns True if index exists, false otherwise
+
*/
+
export async function indexExists<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
indexName: string,
+
): Promise<boolean> {
+
const index = await getIndex(collection, indexName);
+
return index !== null;
+
}
+
+
/**
+
* Synchronize indexes - create indexes if they don't exist, update if they differ
+
*
+
* This is useful for ensuring indexes match your schema definition
+
*
+
* @param collection - MongoDB collection
+
* @param indexes - Array of index descriptions to synchronize
+
* @param options - Options for index creation
+
* @returns Array of index names that were created
+
*/
+
export async function syncIndexes<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
indexes: IndexDescription[],
+
options?: CreateIndexesOptions,
+
): Promise<string[]> {
+
const existingIndexes = await listIndexes(collection);
+
const indexesToCreate: IndexDescription[] = [];
+
+
for (const index of indexes) {
+
const indexName = index.name || generateIndexName(index.key);
+
const existingIndex = existingIndexes.find(
+
(idx) => idx.name === indexName,
+
);
+
+
if (!existingIndex) {
+
indexesToCreate.push(index);
+
} else if (
+
JSON.stringify(existingIndex.key) !== JSON.stringify(index.key)
+
) {
+
// Index exists but keys differ - drop and recreate
+
await dropIndex(collection, indexName);
+
indexesToCreate.push(index);
+
}
+
// If index exists and matches, skip it
+
}
+
+
const created: string[] = [];
+
if (indexesToCreate.length > 0) {
+
const names = await createIndexes(collection, indexesToCreate, options);
+
created.push(...names);
+
}
+
+
return created;
+
}
+
+
/**
+
* Generate index name from key specification
+
*
+
* @param keys - Index specification
+
* @returns Generated index name
+
*/
+
export function generateIndexName(keys: IndexSpecification): string {
+
if (typeof keys === "string") {
+
return keys;
+
}
+
const entries = Object.entries(keys as Record<string, number | string>);
+
return entries.map(([field, direction]) => `${field}_${direction}`).join("_");
+
}
+38
model/pagination.ts
···
···
+
import type { Collection, Document, Filter, WithId } from "mongodb";
+
import type { Infer, Schema } from "../types.ts";
+
+
/**
+
* Pagination operations for the Model class
+
*
+
* This module contains pagination-related functionality for finding documents
+
* with skip, limit, and sort options.
+
*/
+
+
/**
+
* Find documents with pagination support
+
*
+
* @param collection - MongoDB collection
+
* @param query - MongoDB query filter
+
* @param options - Pagination options (skip, limit, sort)
+
* @returns Array of matching documents
+
*
+
* @example
+
* ```ts
+
* const users = await findPaginated(collection,
+
* { age: { $gte: 18 } },
+
* { skip: 0, limit: 10, sort: { createdAt: -1 } }
+
* );
+
* ```
+
*/
+
export async function findPaginated<T extends Schema>(
+
collection: Collection<Infer<T>>,
+
query: Filter<Infer<T>>,
+
options: { skip?: number; limit?: number; sort?: Document } = {},
+
): Promise<(WithId<Infer<T>>)[]> {
+
return await collection
+
.find(query)
+
.skip(options.skip ?? 0)
+
.limit(options.limit ?? 10)
+
.sort(options.sort ?? {})
+
.toArray();
+
}
+300
model/validation.ts
···
···
+
import type { z } from "@zod/zod";
+
import type { Infer, Input, Schema } from "../types.ts";
+
import { AsyncValidationError, ValidationError } from "../errors.ts";
+
import type { Document, Filter, UpdateFilter } from "mongodb";
+
+
// Cache frequently reused schema transformations to avoid repeated allocations
+
const partialSchemaCache = new WeakMap<Schema, z.ZodTypeAny>();
+
const defaultsCache = new WeakMap<Schema, Record<string, unknown>>();
+
const updateOperators = [
+
"$set",
+
"$unset",
+
"$inc",
+
"$mul",
+
"$rename",
+
"$min",
+
"$max",
+
"$currentDate",
+
"$push",
+
"$pull",
+
"$addToSet",
+
"$pop",
+
"$bit",
+
"$setOnInsert",
+
];
+
+
function getPartialSchema(schema: Schema): z.ZodTypeAny {
+
const cached = partialSchemaCache.get(schema);
+
if (cached) return cached;
+
const partial = schema.partial();
+
partialSchemaCache.set(schema, partial);
+
return partial;
+
}
+
+
/**
+
* Validate data for insert operations using Zod schema
+
*
+
* @param schema - Zod schema to validate against
+
* @param data - Data to validate
+
* @returns Validated and typed data
+
* @throws {ValidationError} If validation fails
+
* @throws {AsyncValidationError} If async validation is detected
+
*/
+
export function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
+
const result = schema.safeParse(data);
+
+
// Check for async validation
+
if (result instanceof Promise) {
+
throw new AsyncValidationError();
+
}
+
+
if (!result.success) {
+
throw new ValidationError(result.error.issues, "insert");
+
}
+
return result.data as Infer<T>;
+
}
+
+
/**
+
* Validate partial data for update operations using Zod schema
+
*
+
* Important: This function only validates the fields that are provided in the data object.
+
* Unlike parse(), this function does NOT apply defaults for missing fields because
+
* in an update context, missing fields should remain unchanged in the database.
+
*
+
* @param schema - Zod schema to validate against
+
* @param data - Partial data to validate
+
* @returns Validated and typed partial data (only fields present in input)
+
* @throws {ValidationError} If validation fails
+
* @throws {AsyncValidationError} If async validation is detected
+
*/
+
export function parsePartial<T extends Schema>(
+
schema: T,
+
data: Partial<z.infer<T>>,
+
): Partial<z.infer<T>> {
+
if (!data || Object.keys(data).length === 0) {
+
return {};
+
}
+
+
// Get the list of fields actually provided in the input
+
const inputKeys = Object.keys(data);
+
+
const result = getPartialSchema(schema).safeParse(data);
+
+
// Check for async validation
+
if (result instanceof Promise) {
+
throw new AsyncValidationError();
+
}
+
+
if (!result.success) {
+
throw new ValidationError(result.error.issues, "update");
+
}
+
+
// Filter the result to only include fields that were in the input
+
// This prevents defaults from being applied to fields that weren't provided
+
const filtered: Record<string, unknown> = {};
+
for (const key of inputKeys) {
+
if (key in (result.data as Record<string, unknown>)) {
+
filtered[key] = (result.data as Record<string, unknown>)[key];
+
}
+
}
+
+
return filtered as Partial<z.infer<T>>;
+
}
+
+
/**
+
* Validate data for replace operations using Zod schema
+
*
+
* @param schema - Zod schema to validate against
+
* @param data - Data to validate
+
* @returns Validated and typed data
+
* @throws {ValidationError} If validation fails
+
* @throws {AsyncValidationError} If async validation is detected
+
*/
+
export function parseReplace<T extends Schema>(
+
schema: T,
+
data: Input<T>,
+
): Infer<T> {
+
const result = schema.safeParse(data);
+
+
// Check for async validation
+
if (result instanceof Promise) {
+
throw new AsyncValidationError();
+
}
+
+
if (!result.success) {
+
throw new ValidationError(result.error.issues, "replace");
+
}
+
return result.data as Infer<T>;
+
}
+
+
/**
+
* Extract default values from a Zod schema
+
* This parses an empty object through the schema to get all defaults applied
+
*
+
* @param schema - Zod schema to extract defaults from
+
* @returns Object containing all default values from the schema
+
*/
+
export function extractDefaults<T extends Schema>(
+
schema: T,
+
): Partial<Infer<T>> {
+
const cached = defaultsCache.get(schema);
+
if (cached) {
+
return cached as Partial<Infer<T>>;
+
}
+
+
try {
+
// Make all fields optional, then parse empty object to trigger defaults
+
// This allows us to see which fields get default values
+
const partialSchema = getPartialSchema(schema);
+
const result = partialSchema.safeParse({});
+
+
if (result instanceof Promise) {
+
// Cannot extract defaults from async schemas
+
return {};
+
}
+
+
// If successful, the result contains all fields that have defaults
+
// Only include fields that were actually added (have values)
+
if (!result.success) {
+
return {};
+
}
+
+
// Filter to only include fields that got values from defaults
+
// (not undefined, which indicates no default)
+
const defaults: Record<string, unknown> = {};
+
const data = result.data as Record<string, unknown>;
+
+
for (const [key, value] of Object.entries(data)) {
+
if (value !== undefined) {
+
defaults[key] = value;
+
}
+
}
+
defaultsCache.set(schema, defaults as Partial<Infer<Schema>>);
+
return defaults as Partial<Infer<T>>;
+
} catch {
+
return {};
+
}
+
}
+
+
/**
+
* Get all field paths mentioned in an update filter object
+
* This includes fields in $set, $unset, $inc, $push, etc.
+
*
+
* @param update - MongoDB update filter
+
* @returns Set of field paths that are being modified
+
*/
+
function getModifiedFields(update: UpdateFilter<Document>): Set<string> {
+
const fields = new Set<string>();
+
+
for (const op of updateOperators) {
+
if (update[op] && typeof update[op] === "object") {
+
// Add all field names from this operator
+
for (const field of Object.keys(update[op] as Document)) {
+
fields.add(field);
+
}
+
}
+
}
+
+
return fields;
+
}
+
+
/**
+
* Get field paths that are fixed by equality clauses in a query filter.
+
* Only equality-style predicates become part of the inserted document during upsert.
+
*/
+
function getEqualityFields(filter: Filter<Document>): Set<string> {
+
const fields = new Set<string>();
+
+
const collect = (node: Record<string, unknown>) => {
+
for (const [key, value] of Object.entries(node)) {
+
if (key.startsWith("$")) {
+
if (Array.isArray(value)) {
+
for (const item of value) {
+
if (item && typeof item === "object" && !Array.isArray(item)) {
+
collect(item as Record<string, unknown>);
+
}
+
}
+
} else if (value && typeof value === "object") {
+
collect(value as Record<string, unknown>);
+
}
+
continue;
+
}
+
+
if (value && typeof value === "object" && !Array.isArray(value)) {
+
const objectValue = value as Record<string, unknown>;
+
const keys = Object.keys(objectValue);
+
const hasOperator = keys.some((k) => k.startsWith("$"));
+
+
if (hasOperator) {
+
if (Object.prototype.hasOwnProperty.call(objectValue, "$eq")) {
+
fields.add(key);
+
}
+
} else {
+
fields.add(key);
+
}
+
} else {
+
fields.add(key);
+
}
+
}
+
};
+
+
collect(filter as Record<string, unknown>);
+
return fields;
+
}
+
+
/**
+
* Apply schema defaults to an update operation using $setOnInsert
+
*
+
* This is used for upsert operations to ensure defaults are applied when
+
* a new document is created, but not when updating an existing document.
+
*
+
* For each default field:
+
* - If the field is NOT mentioned in any update operator ($set, $inc, etc.)
+
* - If the field is NOT fixed by an equality clause in the query filter
+
* - Add it to $setOnInsert so it's only applied on insert
+
*
+
* @param schema - Zod schema with defaults
+
* @param query - MongoDB query filter
+
* @param update - MongoDB update filter
+
* @returns Modified update filter with defaults in $setOnInsert
+
*/
+
export function applyDefaultsForUpsert<T extends Schema>(
+
schema: T,
+
query: Filter<Infer<T>>,
+
update: UpdateFilter<Infer<T>>,
+
): UpdateFilter<Infer<T>> {
+
// Extract defaults from schema
+
const defaults = extractDefaults(schema);
+
+
// If no defaults, return update unchanged
+
if (Object.keys(defaults).length === 0) {
+
return update;
+
}
+
+
// Get fields that are already being modified
+
const modifiedFields = getModifiedFields(update as UpdateFilter<Document>);
+
const filterEqualityFields = getEqualityFields(query as Filter<Document>);
+
+
// Build $setOnInsert with defaults for unmodified fields
+
const setOnInsert: Partial<Infer<T>> = {};
+
+
for (const [field, value] of Object.entries(defaults)) {
+
// Only add default if field is not already being modified or fixed by filter equality
+
if (!modifiedFields.has(field) && !filterEqualityFields.has(field)) {
+
setOnInsert[field as keyof Infer<T>] = value as Infer<T>[keyof Infer<T>];
+
}
+
}
+
+
// If there are defaults to add, merge them into $setOnInsert
+
if (Object.keys(setOnInsert).length > 0) {
+
return {
+
...update,
+
$setOnInsert: {
+
...(update.$setOnInsert || {}),
+
...setOnInsert,
+
} as Partial<Infer<T>>,
+
};
+
}
+
+
return update;
+
}
-2097
package-lock.json
···
-
{
-
"name": "mizzleorm",
-
"version": "0.0.1",
-
"lockfileVersion": 3,
-
"requires": true,
-
"packages": {
-
"": {
-
"name": "mizzleorm",
-
"version": "0.0.1",
-
"license": "MIT",
-
"dependencies": {
-
"mongodb": "^6.8.0",
-
"zod": "^3.23.8"
-
},
-
"devDependencies": {
-
"@types/mongodb": "^4.0.7",
-
"@types/node": "^20.14.10",
-
"ts-node": "^10.9.2",
-
"typescript": "^5.5.3",
-
"vitest": "^1.6.0"
-
}
-
},
-
"node_modules/@cspotcode/source-map-support": {
-
"version": "0.8.1",
-
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
-
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@jridgewell/trace-mapping": "0.3.9"
-
},
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/aix-ppc64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
-
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
-
"cpu": [
-
"ppc64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"aix"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/android-arm": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
-
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
-
"cpu": [
-
"arm"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"android"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/android-arm64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
-
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"android"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/android-x64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
-
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"android"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/darwin-arm64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
-
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"darwin"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/darwin-x64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
-
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"darwin"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/freebsd-arm64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
-
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"freebsd"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/freebsd-x64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
-
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"freebsd"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/linux-arm": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
-
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
-
"cpu": [
-
"arm"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/linux-arm64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
-
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/linux-ia32": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
-
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
-
"cpu": [
-
"ia32"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/linux-loong64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
-
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
-
"cpu": [
-
"loong64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/linux-mips64el": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
-
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
-
"cpu": [
-
"mips64el"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/linux-ppc64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
-
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
-
"cpu": [
-
"ppc64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/linux-riscv64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
-
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
-
"cpu": [
-
"riscv64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/linux-s390x": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
-
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
-
"cpu": [
-
"s390x"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/linux-x64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
-
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/netbsd-x64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
-
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"netbsd"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/openbsd-x64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
-
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"openbsd"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/sunos-x64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
-
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"sunos"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/win32-arm64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
-
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"win32"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/win32-ia32": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
-
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
-
"cpu": [
-
"ia32"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"win32"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@esbuild/win32-x64": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
-
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"win32"
-
],
-
"engines": {
-
"node": ">=12"
-
}
-
},
-
"node_modules/@jest/schemas": {
-
"version": "29.6.3",
-
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
-
"integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@sinclair/typebox": "^0.27.8"
-
},
-
"engines": {
-
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
-
}
-
},
-
"node_modules/@jridgewell/resolve-uri": {
-
"version": "3.1.2",
-
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
-
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=6.0.0"
-
}
-
},
-
"node_modules/@jridgewell/sourcemap-codec": {
-
"version": "1.5.4",
-
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
-
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/@jridgewell/trace-mapping": {
-
"version": "0.3.9",
-
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
-
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@jridgewell/resolve-uri": "^3.0.3",
-
"@jridgewell/sourcemap-codec": "^1.4.10"
-
}
-
},
-
"node_modules/@mongodb-js/saslprep": {
-
"version": "1.3.0",
-
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz",
-
"integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==",
-
"license": "MIT",
-
"dependencies": {
-
"sparse-bitfield": "^3.0.3"
-
}
-
},
-
"node_modules/@rollup/rollup-android-arm-eabi": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz",
-
"integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==",
-
"cpu": [
-
"arm"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"android"
-
]
-
},
-
"node_modules/@rollup/rollup-android-arm64": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz",
-
"integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"android"
-
]
-
},
-
"node_modules/@rollup/rollup-darwin-arm64": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz",
-
"integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"darwin"
-
]
-
},
-
"node_modules/@rollup/rollup-darwin-x64": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz",
-
"integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"darwin"
-
]
-
},
-
"node_modules/@rollup/rollup-freebsd-arm64": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz",
-
"integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"freebsd"
-
]
-
},
-
"node_modules/@rollup/rollup-freebsd-x64": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz",
-
"integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"freebsd"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz",
-
"integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==",
-
"cpu": [
-
"arm"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz",
-
"integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==",
-
"cpu": [
-
"arm"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-arm64-gnu": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz",
-
"integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-arm64-musl": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz",
-
"integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz",
-
"integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==",
-
"cpu": [
-
"loong64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz",
-
"integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==",
-
"cpu": [
-
"ppc64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz",
-
"integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==",
-
"cpu": [
-
"riscv64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-riscv64-musl": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz",
-
"integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==",
-
"cpu": [
-
"riscv64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-s390x-gnu": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz",
-
"integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==",
-
"cpu": [
-
"s390x"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-x64-gnu": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz",
-
"integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-linux-x64-musl": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz",
-
"integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"linux"
-
]
-
},
-
"node_modules/@rollup/rollup-win32-arm64-msvc": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz",
-
"integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==",
-
"cpu": [
-
"arm64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"win32"
-
]
-
},
-
"node_modules/@rollup/rollup-win32-ia32-msvc": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz",
-
"integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==",
-
"cpu": [
-
"ia32"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"win32"
-
]
-
},
-
"node_modules/@rollup/rollup-win32-x64-msvc": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz",
-
"integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==",
-
"cpu": [
-
"x64"
-
],
-
"dev": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"win32"
-
]
-
},
-
"node_modules/@sinclair/typebox": {
-
"version": "0.27.8",
-
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
-
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/@tsconfig/node10": {
-
"version": "1.0.11",
-
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
-
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/@tsconfig/node12": {
-
"version": "1.0.11",
-
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
-
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/@tsconfig/node14": {
-
"version": "1.0.3",
-
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
-
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/@tsconfig/node16": {
-
"version": "1.0.4",
-
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
-
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/@types/estree": {
-
"version": "1.0.8",
-
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
-
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/@types/mongodb": {
-
"version": "4.0.7",
-
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-4.0.7.tgz",
-
"integrity": "sha512-lPUYPpzA43baXqnd36cZ9xxorprybxXDzteVKCPAdp14ppHtFJHnXYvNpmBvtMUTb5fKXVv6sVbzo1LHkWhJlw==",
-
"deprecated": "mongodb provides its own types. @types/mongodb is no longer needed.",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"mongodb": "*"
-
}
-
},
-
"node_modules/@types/node": {
-
"version": "20.19.8",
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.8.tgz",
-
"integrity": "sha512-HzbgCY53T6bfu4tT7Aq3TvViJyHjLjPNaAS3HOuMc9pw97KHsUtXNX4L+wu59g1WnjsZSko35MbEqnO58rihhw==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"undici-types": "~6.21.0"
-
}
-
},
-
"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/@vitest/expect": {
-
"version": "1.6.1",
-
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
-
"integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@vitest/spy": "1.6.1",
-
"@vitest/utils": "1.6.1",
-
"chai": "^4.3.10"
-
},
-
"funding": {
-
"url": "https://opencollective.com/vitest"
-
}
-
},
-
"node_modules/@vitest/runner": {
-
"version": "1.6.1",
-
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz",
-
"integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@vitest/utils": "1.6.1",
-
"p-limit": "^5.0.0",
-
"pathe": "^1.1.1"
-
},
-
"funding": {
-
"url": "https://opencollective.com/vitest"
-
}
-
},
-
"node_modules/@vitest/snapshot": {
-
"version": "1.6.1",
-
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz",
-
"integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"magic-string": "^0.30.5",
-
"pathe": "^1.1.1",
-
"pretty-format": "^29.7.0"
-
},
-
"funding": {
-
"url": "https://opencollective.com/vitest"
-
}
-
},
-
"node_modules/@vitest/spy": {
-
"version": "1.6.1",
-
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz",
-
"integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"tinyspy": "^2.2.0"
-
},
-
"funding": {
-
"url": "https://opencollective.com/vitest"
-
}
-
},
-
"node_modules/@vitest/utils": {
-
"version": "1.6.1",
-
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz",
-
"integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"diff-sequences": "^29.6.3",
-
"estree-walker": "^3.0.3",
-
"loupe": "^2.3.7",
-
"pretty-format": "^29.7.0"
-
},
-
"funding": {
-
"url": "https://opencollective.com/vitest"
-
}
-
},
-
"node_modules/acorn": {
-
"version": "8.15.0",
-
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
-
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
-
"dev": true,
-
"license": "MIT",
-
"bin": {
-
"acorn": "bin/acorn"
-
},
-
"engines": {
-
"node": ">=0.4.0"
-
}
-
},
-
"node_modules/acorn-walk": {
-
"version": "8.3.4",
-
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
-
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"acorn": "^8.11.0"
-
},
-
"engines": {
-
"node": ">=0.4.0"
-
}
-
},
-
"node_modules/ansi-styles": {
-
"version": "5.2.0",
-
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
-
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=10"
-
},
-
"funding": {
-
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
-
}
-
},
-
"node_modules/arg": {
-
"version": "4.1.3",
-
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
-
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/assertion-error": {
-
"version": "1.1.0",
-
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
-
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": "*"
-
}
-
},
-
"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/cac": {
-
"version": "6.7.14",
-
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
-
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=8"
-
}
-
},
-
"node_modules/chai": {
-
"version": "4.5.0",
-
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
-
"integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"assertion-error": "^1.1.0",
-
"check-error": "^1.0.3",
-
"deep-eql": "^4.1.3",
-
"get-func-name": "^2.0.2",
-
"loupe": "^2.3.6",
-
"pathval": "^1.1.1",
-
"type-detect": "^4.1.0"
-
},
-
"engines": {
-
"node": ">=4"
-
}
-
},
-
"node_modules/check-error": {
-
"version": "1.0.3",
-
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
-
"integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"get-func-name": "^2.0.2"
-
},
-
"engines": {
-
"node": "*"
-
}
-
},
-
"node_modules/confbox": {
-
"version": "0.1.8",
-
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
-
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/create-require": {
-
"version": "1.1.1",
-
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
-
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/cross-spawn": {
-
"version": "7.0.6",
-
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
-
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"path-key": "^3.1.0",
-
"shebang-command": "^2.0.0",
-
"which": "^2.0.1"
-
},
-
"engines": {
-
"node": ">= 8"
-
}
-
},
-
"node_modules/debug": {
-
"version": "4.4.1",
-
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
-
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"ms": "^2.1.3"
-
},
-
"engines": {
-
"node": ">=6.0"
-
},
-
"peerDependenciesMeta": {
-
"supports-color": {
-
"optional": true
-
}
-
}
-
},
-
"node_modules/deep-eql": {
-
"version": "4.1.4",
-
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
-
"integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"type-detect": "^4.0.0"
-
},
-
"engines": {
-
"node": ">=6"
-
}
-
},
-
"node_modules/diff": {
-
"version": "4.0.2",
-
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
-
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
-
"dev": true,
-
"license": "BSD-3-Clause",
-
"engines": {
-
"node": ">=0.3.1"
-
}
-
},
-
"node_modules/diff-sequences": {
-
"version": "29.6.3",
-
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
-
"integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
-
}
-
},
-
"node_modules/esbuild": {
-
"version": "0.21.5",
-
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
-
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
-
"dev": true,
-
"hasInstallScript": true,
-
"license": "MIT",
-
"bin": {
-
"esbuild": "bin/esbuild"
-
},
-
"engines": {
-
"node": ">=12"
-
},
-
"optionalDependencies": {
-
"@esbuild/aix-ppc64": "0.21.5",
-
"@esbuild/android-arm": "0.21.5",
-
"@esbuild/android-arm64": "0.21.5",
-
"@esbuild/android-x64": "0.21.5",
-
"@esbuild/darwin-arm64": "0.21.5",
-
"@esbuild/darwin-x64": "0.21.5",
-
"@esbuild/freebsd-arm64": "0.21.5",
-
"@esbuild/freebsd-x64": "0.21.5",
-
"@esbuild/linux-arm": "0.21.5",
-
"@esbuild/linux-arm64": "0.21.5",
-
"@esbuild/linux-ia32": "0.21.5",
-
"@esbuild/linux-loong64": "0.21.5",
-
"@esbuild/linux-mips64el": "0.21.5",
-
"@esbuild/linux-ppc64": "0.21.5",
-
"@esbuild/linux-riscv64": "0.21.5",
-
"@esbuild/linux-s390x": "0.21.5",
-
"@esbuild/linux-x64": "0.21.5",
-
"@esbuild/netbsd-x64": "0.21.5",
-
"@esbuild/openbsd-x64": "0.21.5",
-
"@esbuild/sunos-x64": "0.21.5",
-
"@esbuild/win32-arm64": "0.21.5",
-
"@esbuild/win32-ia32": "0.21.5",
-
"@esbuild/win32-x64": "0.21.5"
-
}
-
},
-
"node_modules/estree-walker": {
-
"version": "3.0.3",
-
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
-
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@types/estree": "^1.0.0"
-
}
-
},
-
"node_modules/execa": {
-
"version": "8.0.1",
-
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
-
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"cross-spawn": "^7.0.3",
-
"get-stream": "^8.0.1",
-
"human-signals": "^5.0.0",
-
"is-stream": "^3.0.0",
-
"merge-stream": "^2.0.0",
-
"npm-run-path": "^5.1.0",
-
"onetime": "^6.0.0",
-
"signal-exit": "^4.1.0",
-
"strip-final-newline": "^3.0.0"
-
},
-
"engines": {
-
"node": ">=16.17"
-
},
-
"funding": {
-
"url": "https://github.com/sindresorhus/execa?sponsor=1"
-
}
-
},
-
"node_modules/fsevents": {
-
"version": "2.3.3",
-
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-
"dev": true,
-
"hasInstallScript": true,
-
"license": "MIT",
-
"optional": true,
-
"os": [
-
"darwin"
-
],
-
"engines": {
-
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-
}
-
},
-
"node_modules/get-func-name": {
-
"version": "2.0.2",
-
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
-
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": "*"
-
}
-
},
-
"node_modules/get-stream": {
-
"version": "8.0.1",
-
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
-
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=16"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/human-signals": {
-
"version": "5.0.0",
-
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
-
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
-
"dev": true,
-
"license": "Apache-2.0",
-
"engines": {
-
"node": ">=16.17.0"
-
}
-
},
-
"node_modules/is-stream": {
-
"version": "3.0.0",
-
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
-
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/isexe": {
-
"version": "2.0.0",
-
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-
"dev": true,
-
"license": "ISC"
-
},
-
"node_modules/js-tokens": {
-
"version": "9.0.1",
-
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
-
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/local-pkg": {
-
"version": "0.5.1",
-
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
-
"integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"mlly": "^1.7.3",
-
"pkg-types": "^1.2.1"
-
},
-
"engines": {
-
"node": ">=14"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/antfu"
-
}
-
},
-
"node_modules/loupe": {
-
"version": "2.3.7",
-
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
-
"integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"get-func-name": "^2.0.1"
-
}
-
},
-
"node_modules/magic-string": {
-
"version": "0.30.17",
-
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
-
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@jridgewell/sourcemap-codec": "^1.5.0"
-
}
-
},
-
"node_modules/make-error": {
-
"version": "1.3.6",
-
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
-
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
-
"dev": true,
-
"license": "ISC"
-
},
-
"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-stream": {
-
"version": "2.0.0",
-
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
-
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/mimic-fn": {
-
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
-
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=12"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/mlly": {
-
"version": "1.7.4",
-
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
-
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"acorn": "^8.14.0",
-
"pathe": "^2.0.1",
-
"pkg-types": "^1.3.0",
-
"ufo": "^1.5.4"
-
}
-
},
-
"node_modules/mlly/node_modules/pathe": {
-
"version": "2.0.3",
-
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
-
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/mongodb": {
-
"version": "6.17.0",
-
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz",
-
"integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==",
-
"license": "Apache-2.0",
-
"dependencies": {
-
"@mongodb-js/saslprep": "^1.1.9",
-
"bson": "^6.10.4",
-
"mongodb-connection-string-url": "^3.0.0"
-
},
-
"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.2.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.1.3",
-
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/nanoid": {
-
"version": "3.3.11",
-
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
-
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
-
"dev": true,
-
"funding": [
-
{
-
"type": "github",
-
"url": "https://github.com/sponsors/ai"
-
}
-
],
-
"license": "MIT",
-
"bin": {
-
"nanoid": "bin/nanoid.cjs"
-
},
-
"engines": {
-
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
-
}
-
},
-
"node_modules/npm-run-path": {
-
"version": "5.3.0",
-
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
-
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"path-key": "^4.0.0"
-
},
-
"engines": {
-
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/npm-run-path/node_modules/path-key": {
-
"version": "4.0.0",
-
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
-
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=12"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/onetime": {
-
"version": "6.0.0",
-
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
-
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"mimic-fn": "^4.0.0"
-
},
-
"engines": {
-
"node": ">=12"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/p-limit": {
-
"version": "5.0.0",
-
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
-
"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"yocto-queue": "^1.0.0"
-
},
-
"engines": {
-
"node": ">=18"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/path-key": {
-
"version": "3.1.1",
-
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
-
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=8"
-
}
-
},
-
"node_modules/pathe": {
-
"version": "1.1.2",
-
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
-
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/pathval": {
-
"version": "1.1.1",
-
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
-
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": "*"
-
}
-
},
-
"node_modules/picocolors": {
-
"version": "1.1.1",
-
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
-
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
-
"dev": true,
-
"license": "ISC"
-
},
-
"node_modules/pkg-types": {
-
"version": "1.3.1",
-
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
-
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"confbox": "^0.1.8",
-
"mlly": "^1.7.4",
-
"pathe": "^2.0.1"
-
}
-
},
-
"node_modules/pkg-types/node_modules/pathe": {
-
"version": "2.0.3",
-
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
-
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/postcss": {
-
"version": "8.5.6",
-
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
-
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
-
"dev": true,
-
"funding": [
-
{
-
"type": "opencollective",
-
"url": "https://opencollective.com/postcss/"
-
},
-
{
-
"type": "tidelift",
-
"url": "https://tidelift.com/funding/github/npm/postcss"
-
},
-
{
-
"type": "github",
-
"url": "https://github.com/sponsors/ai"
-
}
-
],
-
"license": "MIT",
-
"dependencies": {
-
"nanoid": "^3.3.11",
-
"picocolors": "^1.1.1",
-
"source-map-js": "^1.2.1"
-
},
-
"engines": {
-
"node": "^10 || ^12 || >=14"
-
}
-
},
-
"node_modules/pretty-format": {
-
"version": "29.7.0",
-
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
-
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@jest/schemas": "^29.6.3",
-
"ansi-styles": "^5.0.0",
-
"react-is": "^18.0.0"
-
},
-
"engines": {
-
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
-
}
-
},
-
"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/react-is": {
-
"version": "18.3.1",
-
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
-
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/rollup": {
-
"version": "4.45.1",
-
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
-
"integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@types/estree": "1.0.8"
-
},
-
"bin": {
-
"rollup": "dist/bin/rollup"
-
},
-
"engines": {
-
"node": ">=18.0.0",
-
"npm": ">=8.0.0"
-
},
-
"optionalDependencies": {
-
"@rollup/rollup-android-arm-eabi": "4.45.1",
-
"@rollup/rollup-android-arm64": "4.45.1",
-
"@rollup/rollup-darwin-arm64": "4.45.1",
-
"@rollup/rollup-darwin-x64": "4.45.1",
-
"@rollup/rollup-freebsd-arm64": "4.45.1",
-
"@rollup/rollup-freebsd-x64": "4.45.1",
-
"@rollup/rollup-linux-arm-gnueabihf": "4.45.1",
-
"@rollup/rollup-linux-arm-musleabihf": "4.45.1",
-
"@rollup/rollup-linux-arm64-gnu": "4.45.1",
-
"@rollup/rollup-linux-arm64-musl": "4.45.1",
-
"@rollup/rollup-linux-loongarch64-gnu": "4.45.1",
-
"@rollup/rollup-linux-powerpc64le-gnu": "4.45.1",
-
"@rollup/rollup-linux-riscv64-gnu": "4.45.1",
-
"@rollup/rollup-linux-riscv64-musl": "4.45.1",
-
"@rollup/rollup-linux-s390x-gnu": "4.45.1",
-
"@rollup/rollup-linux-x64-gnu": "4.45.1",
-
"@rollup/rollup-linux-x64-musl": "4.45.1",
-
"@rollup/rollup-win32-arm64-msvc": "4.45.1",
-
"@rollup/rollup-win32-ia32-msvc": "4.45.1",
-
"@rollup/rollup-win32-x64-msvc": "4.45.1",
-
"fsevents": "~2.3.2"
-
}
-
},
-
"node_modules/shebang-command": {
-
"version": "2.0.0",
-
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
-
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"shebang-regex": "^3.0.0"
-
},
-
"engines": {
-
"node": ">=8"
-
}
-
},
-
"node_modules/shebang-regex": {
-
"version": "3.0.0",
-
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
-
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=8"
-
}
-
},
-
"node_modules/siginfo": {
-
"version": "2.0.0",
-
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
-
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
-
"dev": true,
-
"license": "ISC"
-
},
-
"node_modules/signal-exit": {
-
"version": "4.1.0",
-
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
-
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
-
"dev": true,
-
"license": "ISC",
-
"engines": {
-
"node": ">=14"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/isaacs"
-
}
-
},
-
"node_modules/source-map-js": {
-
"version": "1.2.1",
-
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
-
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
-
"dev": true,
-
"license": "BSD-3-Clause",
-
"engines": {
-
"node": ">=0.10.0"
-
}
-
},
-
"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/stackback": {
-
"version": "0.0.2",
-
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
-
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/std-env": {
-
"version": "3.9.0",
-
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
-
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/strip-final-newline": {
-
"version": "3.0.0",
-
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
-
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=12"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/strip-literal": {
-
"version": "2.1.1",
-
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz",
-
"integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"js-tokens": "^9.0.1"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/antfu"
-
}
-
},
-
"node_modules/tinybench": {
-
"version": "2.9.0",
-
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
-
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/tinypool": {
-
"version": "0.8.4",
-
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
-
"integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=14.0.0"
-
}
-
},
-
"node_modules/tinyspy": {
-
"version": "2.2.1",
-
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
-
"integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=14.0.0"
-
}
-
},
-
"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/ts-node": {
-
"version": "10.9.2",
-
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
-
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@cspotcode/source-map-support": "^0.8.0",
-
"@tsconfig/node10": "^1.0.7",
-
"@tsconfig/node12": "^1.0.7",
-
"@tsconfig/node14": "^1.0.0",
-
"@tsconfig/node16": "^1.0.2",
-
"acorn": "^8.4.1",
-
"acorn-walk": "^8.1.1",
-
"arg": "^4.1.0",
-
"create-require": "^1.1.0",
-
"diff": "^4.0.1",
-
"make-error": "^1.1.1",
-
"v8-compile-cache-lib": "^3.0.1",
-
"yn": "3.1.1"
-
},
-
"bin": {
-
"ts-node": "dist/bin.js",
-
"ts-node-cwd": "dist/bin-cwd.js",
-
"ts-node-esm": "dist/bin-esm.js",
-
"ts-node-script": "dist/bin-script.js",
-
"ts-node-transpile-only": "dist/bin-transpile.js",
-
"ts-script": "dist/bin-script-deprecated.js"
-
},
-
"peerDependencies": {
-
"@swc/core": ">=1.2.50",
-
"@swc/wasm": ">=1.2.50",
-
"@types/node": "*",
-
"typescript": ">=2.7"
-
},
-
"peerDependenciesMeta": {
-
"@swc/core": {
-
"optional": true
-
},
-
"@swc/wasm": {
-
"optional": true
-
}
-
}
-
},
-
"node_modules/type-detect": {
-
"version": "4.1.0",
-
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
-
"integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=4"
-
}
-
},
-
"node_modules/typescript": {
-
"version": "5.8.3",
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
-
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
-
"dev": true,
-
"license": "Apache-2.0",
-
"bin": {
-
"tsc": "bin/tsc",
-
"tsserver": "bin/tsserver"
-
},
-
"engines": {
-
"node": ">=14.17"
-
}
-
},
-
"node_modules/ufo": {
-
"version": "1.6.1",
-
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
-
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/undici-types": {
-
"version": "6.21.0",
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
-
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/v8-compile-cache-lib": {
-
"version": "3.0.1",
-
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
-
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
-
"dev": true,
-
"license": "MIT"
-
},
-
"node_modules/vite": {
-
"version": "5.4.19",
-
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
-
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"esbuild": "^0.21.3",
-
"postcss": "^8.4.43",
-
"rollup": "^4.20.0"
-
},
-
"bin": {
-
"vite": "bin/vite.js"
-
},
-
"engines": {
-
"node": "^18.0.0 || >=20.0.0"
-
},
-
"funding": {
-
"url": "https://github.com/vitejs/vite?sponsor=1"
-
},
-
"optionalDependencies": {
-
"fsevents": "~2.3.3"
-
},
-
"peerDependencies": {
-
"@types/node": "^18.0.0 || >=20.0.0",
-
"less": "*",
-
"lightningcss": "^1.21.0",
-
"sass": "*",
-
"sass-embedded": "*",
-
"stylus": "*",
-
"sugarss": "*",
-
"terser": "^5.4.0"
-
},
-
"peerDependenciesMeta": {
-
"@types/node": {
-
"optional": true
-
},
-
"less": {
-
"optional": true
-
},
-
"lightningcss": {
-
"optional": true
-
},
-
"sass": {
-
"optional": true
-
},
-
"sass-embedded": {
-
"optional": true
-
},
-
"stylus": {
-
"optional": true
-
},
-
"sugarss": {
-
"optional": true
-
},
-
"terser": {
-
"optional": true
-
}
-
}
-
},
-
"node_modules/vite-node": {
-
"version": "1.6.1",
-
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz",
-
"integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"cac": "^6.7.14",
-
"debug": "^4.3.4",
-
"pathe": "^1.1.1",
-
"picocolors": "^1.0.0",
-
"vite": "^5.0.0"
-
},
-
"bin": {
-
"vite-node": "vite-node.mjs"
-
},
-
"engines": {
-
"node": "^18.0.0 || >=20.0.0"
-
},
-
"funding": {
-
"url": "https://opencollective.com/vitest"
-
}
-
},
-
"node_modules/vitest": {
-
"version": "1.6.1",
-
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz",
-
"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"@vitest/expect": "1.6.1",
-
"@vitest/runner": "1.6.1",
-
"@vitest/snapshot": "1.6.1",
-
"@vitest/spy": "1.6.1",
-
"@vitest/utils": "1.6.1",
-
"acorn-walk": "^8.3.2",
-
"chai": "^4.3.10",
-
"debug": "^4.3.4",
-
"execa": "^8.0.1",
-
"local-pkg": "^0.5.0",
-
"magic-string": "^0.30.5",
-
"pathe": "^1.1.1",
-
"picocolors": "^1.0.0",
-
"std-env": "^3.5.0",
-
"strip-literal": "^2.0.0",
-
"tinybench": "^2.5.1",
-
"tinypool": "^0.8.3",
-
"vite": "^5.0.0",
-
"vite-node": "1.6.1",
-
"why-is-node-running": "^2.2.2"
-
},
-
"bin": {
-
"vitest": "vitest.mjs"
-
},
-
"engines": {
-
"node": "^18.0.0 || >=20.0.0"
-
},
-
"funding": {
-
"url": "https://opencollective.com/vitest"
-
},
-
"peerDependencies": {
-
"@edge-runtime/vm": "*",
-
"@types/node": "^18.0.0 || >=20.0.0",
-
"@vitest/browser": "1.6.1",
-
"@vitest/ui": "1.6.1",
-
"happy-dom": "*",
-
"jsdom": "*"
-
},
-
"peerDependenciesMeta": {
-
"@edge-runtime/vm": {
-
"optional": true
-
},
-
"@types/node": {
-
"optional": true
-
},
-
"@vitest/browser": {
-
"optional": true
-
},
-
"@vitest/ui": {
-
"optional": true
-
},
-
"happy-dom": {
-
"optional": true
-
},
-
"jsdom": {
-
"optional": true
-
}
-
}
-
},
-
"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"
-
}
-
},
-
"node_modules/which": {
-
"version": "2.0.2",
-
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
-
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-
"dev": true,
-
"license": "ISC",
-
"dependencies": {
-
"isexe": "^2.0.0"
-
},
-
"bin": {
-
"node-which": "bin/node-which"
-
},
-
"engines": {
-
"node": ">= 8"
-
}
-
},
-
"node_modules/why-is-node-running": {
-
"version": "2.3.0",
-
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
-
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
-
"dev": true,
-
"license": "MIT",
-
"dependencies": {
-
"siginfo": "^2.0.0",
-
"stackback": "0.0.2"
-
},
-
"bin": {
-
"why-is-node-running": "cli.js"
-
},
-
"engines": {
-
"node": ">=8"
-
}
-
},
-
"node_modules/yn": {
-
"version": "3.1.1",
-
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
-
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=6"
-
}
-
},
-
"node_modules/yocto-queue": {
-
"version": "1.2.1",
-
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
-
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
-
"dev": true,
-
"license": "MIT",
-
"engines": {
-
"node": ">=12.20"
-
},
-
"funding": {
-
"url": "https://github.com/sponsors/sindresorhus"
-
}
-
},
-
"node_modules/zod": {
-
"version": "3.25.76",
-
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
-
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
-
"license": "MIT",
-
"funding": {
-
"url": "https://github.com/sponsors/colinhacks"
-
}
-
}
-
}
-
}
···
-36
package.json
···
-
{
-
"name": "mizzleorm",
-
"version": "0.0.1",
-
"description": "A lightweight, fully type-safe MongoDB ORM in TypeScript, inspired by Drizzle ORM.",
-
"main": "dist/index.js",
-
"types": "dist/index.d.ts",
-
"scripts": {
-
"build": "tsc",
-
"test": "vitest",
-
"example": "ts-node examples/user.ts"
-
},
-
"keywords": [
-
"mongodb",
-
"orm",
-
"typescript",
-
"zod",
-
"type-safe"
-
],
-
"author": "dev-shahed",
-
"license": "MIT",
-
"devDependencies": {
-
"@types/mongodb": "^4.0.7",
-
"@types/node": "^20.14.10",
-
"ts-node": "^10.9.2",
-
"typescript": "^5.5.3",
-
"vitest": "^1.6.0"
-
},
-
"dependencies": {
-
"mongodb": "^6.8.0",
-
"zod": "^3.23.8"
-
},
-
"directories": {
-
"example": "examples",
-
"test": "tests"
-
}
-
}
···
-85
src/client.js
···
-
"use strict";
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
-
return new (P || (P = Promise))(function (resolve, reject) {
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
-
});
-
};
-
var __generator = (this && this.__generator) || function (thisArg, body) {
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
-
function verb(n) { return function (v) { return step([n, v]); }; }
-
function step(op) {
-
if (f) throw new TypeError("Generator is already executing.");
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
-
if (y = 0, t) op = [op[0] & 2, t.value];
-
switch (op[0]) {
-
case 0: case 1: t = op; break;
-
case 4: _.label++; return { value: op[1], done: false };
-
case 5: _.label++; y = op[1]; op = [0]; continue;
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
-
default:
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
-
if (t[2]) _.ops.pop();
-
_.trys.pop(); continue;
-
}
-
op = body.call(thisArg, _);
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
-
}
-
};
-
Object.defineProperty(exports, "__esModule", { value: true });
-
exports.connect = connect;
-
exports.disconnect = disconnect;
-
exports.getDb = getDb;
-
var mongodb_1 = require("mongodb");
-
var connection = null;
-
function connect(uri, dbName) {
-
return __awaiter(this, void 0, void 0, function () {
-
var client, db;
-
return __generator(this, function (_a) {
-
switch (_a.label) {
-
case 0:
-
if (connection) {
-
return [2 /*return*/, connection];
-
}
-
client = new mongodb_1.MongoClient(uri);
-
return [4 /*yield*/, client.connect()];
-
case 1:
-
_a.sent();
-
db = client.db(dbName);
-
connection = { client: client, db: db };
-
return [2 /*return*/, connection];
-
}
-
});
-
});
-
}
-
function disconnect() {
-
return __awaiter(this, void 0, void 0, function () {
-
return __generator(this, function (_a) {
-
switch (_a.label) {
-
case 0:
-
if (!connection) return [3 /*break*/, 2];
-
return [4 /*yield*/, connection.client.close()];
-
case 1:
-
_a.sent();
-
connection = null;
-
_a.label = 2;
-
case 2: return [2 /*return*/];
-
}
-
});
-
});
-
}
-
function getDb() {
-
if (!connection) {
-
throw new Error('MongoDB not connected. Call connect() first.');
-
}
-
return connection.db;
-
}
···
-37
src/client.ts
···
-
import { MongoClient, Db } from 'mongodb';
-
-
interface MongoConnection {
-
client: MongoClient;
-
db: Db;
-
}
-
-
let connection: MongoConnection | null = null;
-
-
export async function connect(uri: string, dbName: string): Promise<MongoConnection> {
-
if (connection) {
-
return connection;
-
}
-
-
const client = new MongoClient(uri);
-
await client.connect();
-
const db = client.db(dbName);
-
-
connection = { client, db };
-
return connection;
-
}
-
-
export async function disconnect(): Promise<void> {
-
if (connection) {
-
await connection.client.close();
-
connection = null;
-
}
-
}
-
-
export function getDb(): Db {
-
if (!connection) {
-
throw new Error('MongoDB not connected. Call connect() first.');
-
}
-
return connection.db;
-
}
-
-
···
-10
src/index.js
···
-
"use strict";
-
Object.defineProperty(exports, "__esModule", { value: true });
-
exports.MongoModel = exports.disconnect = exports.connect = exports.defineModel = void 0;
-
var schema_1 = require("./schema");
-
Object.defineProperty(exports, "defineModel", { enumerable: true, get: function () { return schema_1.defineModel; } });
-
var client_1 = require("./client");
-
Object.defineProperty(exports, "connect", { enumerable: true, get: function () { return client_1.connect; } });
-
Object.defineProperty(exports, "disconnect", { enumerable: true, get: function () { return client_1.disconnect; } });
-
var model_1 = require("./model");
-
Object.defineProperty(exports, "MongoModel", { enumerable: true, get: function () { return model_1.MongoModel; } });
···
-5
src/index.ts
···
-
export { defineModel, InferModel, InsertType } from './schema';
-
export { connect, disconnect } from './client';
-
export { MongoModel } from './model';
-
-
···
-73
src/model.js
···
-
"use strict";
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
-
return new (P || (P = Promise))(function (resolve, reject) {
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
-
});
-
};
-
var __generator = (this && this.__generator) || function (thisArg, body) {
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
-
function verb(n) { return function (v) { return step([n, v]); }; }
-
function step(op) {
-
if (f) throw new TypeError("Generator is already executing.");
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
-
if (y = 0, t) op = [op[0] & 2, t.value];
-
switch (op[0]) {
-
case 0: case 1: t = op; break;
-
case 4: _.label++; return { value: op[1], done: false };
-
case 5: _.label++; y = op[1]; op = [0]; continue;
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
-
default:
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
-
if (t[2]) _.ops.pop();
-
_.trys.pop(); continue;
-
}
-
op = body.call(thisArg, _);
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
-
}
-
};
-
Object.defineProperty(exports, "__esModule", { value: true });
-
exports.MongoModel = void 0;
-
var client_1 = require("./client");
-
var MongoModel = /** @class */ (function () {
-
function MongoModel(collectionName, schema) {
-
this.collection = (0, client_1.getDb)().collection(collectionName);
-
this.schema = schema;
-
}
-
MongoModel.prototype.insertOne = function (data) {
-
return __awaiter(this, void 0, void 0, function () {
-
var validatedData;
-
return __generator(this, function (_a) {
-
validatedData = this.schema.parse(data);
-
return [2 /*return*/, this.collection.insertOne(validatedData)];
-
});
-
});
-
};
-
MongoModel.prototype.find = function (query) {
-
return this.collection.find(query).toArray();
-
};
-
MongoModel.prototype.findOne = function (query) {
-
return this.collection.findOne(query);
-
};
-
MongoModel.prototype.update = function (query, data) {
-
return __awaiter(this, void 0, void 0, function () {
-
return __generator(this, function (_a) {
-
return [2 /*return*/, this.collection.updateMany(query, { $set: data })];
-
});
-
});
-
};
-
MongoModel.prototype.delete = function (query) {
-
return this.collection.deleteMany(query);
-
};
-
return MongoModel;
-
}());
-
exports.MongoModel = MongoModel;
···
-37
src/model.ts
···
-
import { z } from 'zod';
-
import { Collection, InsertOneResult, UpdateResult, DeleteResult, Document, ObjectId, Filter } from 'mongodb';
-
import { getDb } from './client';
-
import { InsertType } from './schema';
-
-
export class MongoModel<T extends z.ZodObject<any>> {
-
private collection: Collection<z.infer<T>>;
-
private schema: T;
-
-
constructor(collectionName: string, schema: T) {
-
this.collection = getDb().collection<z.infer<T>>(collectionName);
-
this.schema = schema;
-
}
-
-
async insertOne(data: InsertType<T>): Promise<InsertOneResult<z.infer<T>>> {
-
const validatedData = this.schema.parse(data);
-
return this.collection.insertOne(validatedData as any);
-
}
-
-
find(query: Filter<z.infer<T>>): Promise<(z.infer<T> & { _id: ObjectId })[]> {
-
return this.collection.find(query).toArray() as Promise<(z.infer<T> & { _id: ObjectId })[]>;
-
}
-
-
findOne(query: Filter<z.infer<T>>): Promise<(z.infer<T> & { _id: ObjectId }) | null> {
-
return this.collection.findOne(query) as Promise<(z.infer<T> & { _id: ObjectId }) | null>;
-
}
-
-
async update(query: Filter<z.infer<T>>, data: Partial<z.infer<T>>): Promise<UpdateResult> {
-
return this.collection.updateMany(query, { $set: data });
-
}
-
-
delete(query: Filter<z.infer<T>>): Promise<DeleteResult> {
-
return this.collection.deleteMany(query);
-
}
-
}
-
-
···
-6
src/schema.js
···
-
"use strict";
-
Object.defineProperty(exports, "__esModule", { value: true });
-
exports.defineModel = defineModel;
-
function defineModel(schema) {
-
return schema;
-
}
···
-12
src/schema.ts
···
-
import { z } from 'zod';
-
import { ObjectId } from 'mongodb';
-
-
export function defineModel<T extends z.ZodObject<any>>(schema: T) {
-
return schema;
-
}
-
-
export type InferModel<T extends z.ZodObject<any>> = z.infer<T> & { _id?: ObjectId };
-
-
export type InsertType<T extends z.ZodObject<any>> = Omit<z.infer<T>, 'createdAt'> & { createdAt?: Date };
-
-
···
+305
tests/connection_test.ts
···
···
+
import { assert, assertEquals, assertExists } from "@std/assert";
+
import {
+
connect,
+
type ConnectOptions,
+
disconnect,
+
healthCheck,
+
} from "../mod.ts";
+
import { MongoMemoryServer } from "mongodb-memory-server-core";
+
+
let mongoServer: MongoMemoryServer | null = null;
+
+
async function setupTestServer() {
+
if (!mongoServer) {
+
mongoServer = await MongoMemoryServer.create();
+
}
+
return mongoServer.getUri();
+
}
+
+
Deno.test.afterEach(async () => {
+
await disconnect();
+
});
+
+
Deno.test.afterAll(async () => {
+
if (mongoServer) {
+
await mongoServer.stop();
+
mongoServer = null;
+
}
+
});
+
+
Deno.test({
+
name: "Connection: Basic - should connect without options",
+
async fn() {
+
const uri = await setupTestServer();
+
const connection = await connect(uri, "test_db");
+
+
assert(connection);
+
assert(connection.client);
+
assert(connection.db);
+
assertEquals(connection.db.databaseName, "test_db");
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Connection: Options - should connect with pooling options",
+
async fn() {
+
const uri = await setupTestServer();
+
const options: ConnectOptions = {
+
maxPoolSize: 10,
+
minPoolSize: 2,
+
maxIdleTimeMS: 30000,
+
connectTimeoutMS: 5000,
+
};
+
+
const connection = await connect(uri, "test_db", options);
+
+
assert(connection);
+
assert(connection.client);
+
assert(connection.db);
+
+
// Verify connection is working
+
const adminDb = connection.db.admin();
+
const serverStatus = await adminDb.serverStatus();
+
assert(serverStatus);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Connection: Singleton - should reuse existing connection",
+
async fn() {
+
const uri = await setupTestServer();
+
+
const connection1 = await connect(uri, "test_db");
+
const connection2 = await connect(uri, "test_db");
+
+
// Should return the same connection instance
+
assertEquals(connection1, connection2);
+
assertEquals(connection1.client, connection2.client);
+
assertEquals(connection1.db, connection2.db);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Connection: Disconnect - should disconnect and allow reconnection",
+
async fn() {
+
const uri = await setupTestServer();
+
+
const connection1 = await connect(uri, "test_db");
+
assert(connection1);
+
+
await disconnect();
+
+
// Should be able to reconnect
+
const connection2 = await connect(uri, "test_db");
+
assert(connection2);
+
+
// Should be a new connection instance
+
assert(connection1 !== connection2);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Connection: Options - should apply maxPoolSize option",
+
async fn() {
+
const uri = await setupTestServer();
+
const options: ConnectOptions = {
+
maxPoolSize: 5,
+
};
+
+
const connection = await connect(uri, "test_db", options);
+
+
// Verify connection works with custom pool size
+
const collections = await connection.db.listCollections().toArray();
+
assert(Array.isArray(collections));
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name:
+
"Connection: Multiple Databases - should handle different database names",
+
async fn() {
+
const uri = await setupTestServer();
+
+
// Connect to first database
+
const connection1 = await connect(uri, "db1");
+
assertEquals(connection1.db.databaseName, "db1");
+
+
// Disconnect first
+
await disconnect();
+
+
// Connect to second database
+
const connection2 = await connect(uri, "db2");
+
assertEquals(connection2.db.databaseName, "db2");
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Health Check: should return unhealthy when not connected",
+
async fn() {
+
const result = await healthCheck();
+
+
assertEquals(result.healthy, false);
+
assertEquals(result.connected, false);
+
assertExists(result.error);
+
assert(result.error?.includes("No active connection"));
+
assertExists(result.timestamp);
+
assertEquals(result.responseTimeMs, undefined);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Health Check: should return healthy when connected",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
const result = await healthCheck();
+
+
assertEquals(result.healthy, true);
+
assertEquals(result.connected, true);
+
assertExists(result.responseTimeMs);
+
assert(result.responseTimeMs! >= 0);
+
assertEquals(result.error, undefined);
+
assertExists(result.timestamp);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Health Check: should measure response time",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
const result = await healthCheck();
+
+
assertEquals(result.healthy, true);
+
assertExists(result.responseTimeMs);
+
// Response time should be reasonable (less than 1 second for in-memory MongoDB)
+
assert(result.responseTimeMs! < 1000);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Health Check: should work multiple times consecutively",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
// Run health check multiple times
+
const results = await Promise.all([
+
healthCheck(),
+
healthCheck(),
+
healthCheck(),
+
]);
+
+
// All should be healthy
+
for (const result of results) {
+
assertEquals(result.healthy, true);
+
assertEquals(result.connected, true);
+
assertExists(result.responseTimeMs);
+
}
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Health Check: should detect disconnection",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
// First check should be healthy
+
let result = await healthCheck();
+
assertEquals(result.healthy, true);
+
+
// Disconnect
+
await disconnect();
+
+
// Second check should be unhealthy
+
result = await healthCheck();
+
assertEquals(result.healthy, false);
+
assertEquals(result.connected, false);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Connection: Retry Options - should accept retry configuration",
+
async fn() {
+
const uri = await setupTestServer();
+
const options: ConnectOptions = {
+
retryReads: true,
+
retryWrites: true,
+
serverSelectionTimeoutMS: 5000,
+
connectTimeoutMS: 5000,
+
};
+
+
const connection = await connect(uri, "test_db", options);
+
+
assert(connection);
+
assert(connection.client);
+
assert(connection.db);
+
+
// Verify connection works with retry options
+
const collections = await connection.db.listCollections().toArray();
+
assert(Array.isArray(collections));
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Connection: Resilience Options - should accept full production config",
+
async fn() {
+
const uri = await setupTestServer();
+
const options: ConnectOptions = {
+
// Pooling
+
maxPoolSize: 10,
+
minPoolSize: 2,
+
+
// Retry logic
+
retryReads: true,
+
retryWrites: true,
+
+
// Timeouts
+
connectTimeoutMS: 10000,
+
socketTimeoutMS: 45000,
+
serverSelectionTimeoutMS: 10000,
+
+
// Resilience
+
maxIdleTimeMS: 30000,
+
heartbeatFrequencyMS: 10000,
+
};
+
+
const connection = await connect(uri, "test_db", options);
+
+
assert(connection);
+
+
// Verify connection is working
+
const adminDb = connection.db.admin();
+
const serverStatus = await adminDb.serverStatus();
+
assert(serverStatus);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+154
tests/crud_test.ts
···
···
+
import { assert, assertEquals, assertExists } from "@std/assert";
+
import { ObjectId } from "mongodb";
+
import {
+
cleanupCollection,
+
createUserModel,
+
setupTestDb,
+
teardownTestDb,
+
type UserInsert,
+
type userSchema,
+
} from "./utils.ts";
+
import type { Model } from "../mod.ts";
+
+
let UserModel: Model<typeof userSchema>;
+
+
Deno.test.beforeAll(async () => {
+
await setupTestDb();
+
UserModel = createUserModel("users_crud");
+
});
+
+
Deno.test.beforeEach(async () => {
+
await cleanupCollection(UserModel);
+
});
+
+
Deno.test.afterAll(async () => {
+
await teardownTestDb();
+
});
+
+
Deno.test({
+
name: "CRUD: Insert - should insert a new user successfully",
+
async fn() {
+
const newUser: UserInsert = {
+
name: "Test User",
+
email: "test@example.com",
+
age: 25,
+
};
+
+
const insertResult = await UserModel.insertOne(newUser);
+
+
assertExists(insertResult.insertedId);
+
console.log("User inserted with ID:", insertResult.insertedId);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "CRUD: Find - should find the inserted user",
+
async fn() {
+
// First insert a user for this test
+
const newUser: UserInsert = {
+
name: "Find Test User",
+
email: "findtest@example.com",
+
age: 30,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
const foundUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertExists(foundUser);
+
assertEquals(foundUser.email, "findtest@example.com");
+
assertEquals(foundUser.name, "Find Test User");
+
assertEquals(foundUser.age, 30);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "CRUD: Update - should update user data",
+
async fn() {
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Update Test User",
+
email: "updatetest@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Update the user
+
const updateResult = await UserModel.update(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ age: 26 },
+
);
+
+
assertEquals(updateResult.modifiedCount, 1);
+
+
// Verify the update
+
const updatedUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertExists(updatedUser);
+
assertEquals(updatedUser.age, 26);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "CRUD: Delete - should delete user successfully",
+
async fn() {
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Delete Test User",
+
email: "deletetest@example.com",
+
age: 35,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Delete the user
+
const deleteResult = await UserModel.delete({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertEquals(deleteResult.deletedCount, 1);
+
+
// Verify deletion
+
const deletedUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertEquals(deletedUser, null);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "CRUD: Find Multiple - should find multiple users",
+
async fn() {
+
// Insert multiple users
+
const users: UserInsert[] = [
+
{ name: "User 1", email: "user1@example.com", age: 20 },
+
{ name: "User 2", email: "user2@example.com", age: 25 },
+
{ name: "User 3", email: "user3@example.com", age: 30 },
+
];
+
+
for (const user of users) {
+
await UserModel.insertOne(user);
+
}
+
+
// Find all users with age >= 25
+
const foundUsers = await UserModel.find({ age: { $gte: 25 } });
+
+
assert(foundUsers.length >= 2);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+401
tests/defaults_test.ts
···
···
+
import { assertEquals, assertExists } from "@std/assert";
+
import { z } from "@zod/zod";
+
import { Model } from "../mod.ts";
+
import { applyDefaultsForUpsert } from "../model/validation.ts";
+
import { setupTestDb, teardownTestDb } from "./utils.ts";
+
+
/**
+
* Test suite for default value handling in different operation types
+
*
+
* This tests the three main cases:
+
* 1. Plain inserts - defaults applied directly
+
* 2. Updates without upsert - defaults NOT applied
+
* 3. Upserts that create - defaults applied via $setOnInsert
+
* 4. Upserts that match - $setOnInsert ignored (correct behavior)
+
* 5. Replace with upsert - defaults applied on creation
+
*/
+
+
// Schema with defaults for testing
+
const productSchema = z.object({
+
name: z.string(),
+
price: z.number().min(0),
+
category: z.string().default("general"),
+
inStock: z.boolean().default(true),
+
createdAt: z.date().default(() => new Date("2024-01-01T00:00:00Z")),
+
tags: z.array(z.string()).default([]),
+
});
+
+
let ProductModel: Model<typeof productSchema>;
+
+
Deno.test.beforeAll(async () => {
+
await setupTestDb();
+
ProductModel = new Model("test_products_defaults", productSchema);
+
});
+
+
Deno.test.beforeEach(async () => {
+
await ProductModel.delete({});
+
});
+
+
Deno.test.afterAll(async () => {
+
await ProductModel.delete({});
+
await teardownTestDb();
+
});
+
+
Deno.test({
+
name: "Defaults: Case 1 - Plain insert applies defaults",
+
async fn() {
+
// Insert without providing optional fields with defaults
+
const result = await ProductModel.insertOne({
+
name: "Widget",
+
price: 29.99,
+
// category, inStock, createdAt, tags not provided - should use defaults
+
});
+
+
assertExists(result.insertedId);
+
+
// Verify defaults were applied
+
const product = await ProductModel.findById(result.insertedId);
+
assertExists(product);
+
+
assertEquals(product.name, "Widget");
+
assertEquals(product.price, 29.99);
+
assertEquals(product.category, "general"); // default
+
assertEquals(product.inStock, true); // default
+
assertExists(product.createdAt); // default function called
+
assertEquals(product.tags, []); // default empty array
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Defaults: Case 2 - Update without upsert does NOT apply defaults",
+
async fn() {
+
// First create a document without defaults (simulate old data)
+
const insertResult = await ProductModel.insertOne({
+
name: "Gadget",
+
price: 19.99,
+
category: "electronics",
+
inStock: false,
+
createdAt: new Date("2023-01-01"),
+
tags: ["test"],
+
});
+
+
assertExists(insertResult.insertedId);
+
+
// Now update it - defaults should NOT be applied
+
await ProductModel.updateOne(
+
{ _id: insertResult.insertedId },
+
{ price: 24.99 },
+
// No upsert flag
+
);
+
+
const updated = await ProductModel.findById(insertResult.insertedId);
+
assertExists(updated);
+
+
assertEquals(updated.price, 24.99); // updated
+
assertEquals(updated.category, "electronics"); // unchanged
+
assertEquals(updated.inStock, false); // unchanged
+
assertEquals(updated.tags, ["test"]); // unchanged
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name:
+
"Defaults: Case 3 - Upsert that creates applies defaults via $setOnInsert",
+
async fn() {
+
// Upsert with a query that won't match - will create new document
+
const result = await ProductModel.updateOne(
+
{ name: "NonExistent" },
+
{ price: 39.99 },
+
{ upsert: true },
+
);
+
+
assertEquals(result.upsertedCount, 1);
+
assertExists(result.upsertedId);
+
+
// Verify the created document has defaults applied
+
const product = await ProductModel.findOne({ name: "NonExistent" });
+
assertExists(product);
+
+
assertEquals(product.price, 39.99); // from $set
+
assertEquals(product.name, "NonExistent"); // from query
+
assertEquals(product.category, "general"); // default via $setOnInsert
+
assertEquals(product.inStock, true); // default via $setOnInsert
+
assertExists(product.createdAt); // default via $setOnInsert
+
assertEquals(product.tags, []); // default via $setOnInsert
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Defaults: Case 4 - Upsert that matches does NOT apply defaults",
+
async fn() {
+
// Create a document first with explicit non-default values
+
const insertResult = await ProductModel.insertOne({
+
name: "ExistingProduct",
+
price: 49.99,
+
category: "premium",
+
inStock: false,
+
createdAt: new Date("2023-06-01"),
+
tags: ["premium", "featured"],
+
});
+
+
assertExists(insertResult.insertedId);
+
+
// Upsert with matching query - should update, not insert
+
const result = await ProductModel.updateOne(
+
{ name: "ExistingProduct" },
+
{ price: 44.99 },
+
{ upsert: true },
+
);
+
+
assertEquals(result.matchedCount, 1);
+
assertEquals(result.modifiedCount, 1);
+
assertEquals(result.upsertedCount, 0); // No insert happened
+
+
// Verify defaults were NOT applied (existing values preserved)
+
const product = await ProductModel.findOne({ name: "ExistingProduct" });
+
assertExists(product);
+
+
assertEquals(product.price, 44.99); // updated via $set
+
assertEquals(product.category, "premium"); // preserved (not overwritten with default)
+
assertEquals(product.inStock, false); // preserved
+
assertEquals(product.tags, ["premium", "featured"]); // preserved
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Defaults: Case 5 - Replace without upsert uses defaults from parse",
+
async fn() {
+
// Create initial document
+
const insertResult = await ProductModel.insertOne({
+
name: "ReplaceMe",
+
price: 10.0,
+
category: "old",
+
inStock: true,
+
createdAt: new Date("2020-01-01"),
+
tags: ["old"],
+
});
+
+
assertExists(insertResult.insertedId);
+
+
// Replace with partial data - defaults should fill in missing fields
+
await ProductModel.replaceOne(
+
{ _id: insertResult.insertedId },
+
{
+
name: "Replaced",
+
price: 15.0,
+
// category, inStock, createdAt, tags not provided - defaults should apply
+
},
+
);
+
+
const product = await ProductModel.findById(insertResult.insertedId);
+
assertExists(product);
+
+
assertEquals(product.name, "Replaced");
+
assertEquals(product.price, 15.0);
+
assertEquals(product.category, "general"); // default applied
+
assertEquals(product.inStock, true); // default applied
+
assertExists(product.createdAt); // default applied
+
assertEquals(product.tags, []); // default applied
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Defaults: Case 6 - Replace with upsert (creates) applies defaults",
+
async fn() {
+
// Replace with upsert on non-existent document
+
const result = await ProductModel.replaceOne(
+
{ name: "NewViaReplace" },
+
{
+
name: "NewViaReplace",
+
price: 99.99,
+
// Missing optional fields - defaults should apply
+
},
+
{ upsert: true },
+
);
+
+
assertEquals(result.upsertedCount, 1);
+
assertExists(result.upsertedId);
+
+
const product = await ProductModel.findOne({ name: "NewViaReplace" });
+
assertExists(product);
+
+
assertEquals(product.name, "NewViaReplace");
+
assertEquals(product.price, 99.99);
+
assertEquals(product.category, "general"); // default
+
assertEquals(product.inStock, true); // default
+
assertExists(product.createdAt); // default
+
assertEquals(product.tags, []); // default
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Defaults: Upsert only applies defaults to unmodified fields",
+
async fn() {
+
// Upsert where we explicitly set some fields that have defaults
+
const result = await ProductModel.updateOne(
+
{ name: "CustomDefaults" },
+
{
+
price: 25.0,
+
category: "custom", // Explicitly setting a field that has a default
+
// inStock not set - should get default
+
},
+
{ upsert: true },
+
);
+
+
assertEquals(result.upsertedCount, 1);
+
+
const product = await ProductModel.findOne({ name: "CustomDefaults" });
+
assertExists(product);
+
+
assertEquals(product.name, "CustomDefaults"); // from query
+
assertEquals(product.price, 25.0); // from $set
+
assertEquals(product.category, "custom"); // from $set (NOT default)
+
assertEquals(product.inStock, true); // default via $setOnInsert
+
assertExists(product.createdAt); // default via $setOnInsert
+
assertEquals(product.tags, []); // default via $setOnInsert
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Defaults: insertMany applies defaults to all documents",
+
async fn() {
+
const result = await ProductModel.insertMany([
+
{ name: "Bulk1", price: 10 },
+
{ name: "Bulk2", price: 20, category: "special" },
+
{ name: "Bulk3", price: 30 },
+
]);
+
+
assertEquals(Object.keys(result.insertedIds).length, 3);
+
+
const products = await ProductModel.find({});
+
assertEquals(products.length, 3);
+
+
// All should have defaults where not provided
+
for (const product of products) {
+
assertExists(product.createdAt);
+
assertEquals(product.inStock, true);
+
assertEquals(product.tags, []);
+
+
if (product.name === "Bulk2") {
+
assertEquals(product.category, "special");
+
} else {
+
assertEquals(product.category, "general");
+
}
+
}
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name:
+
"Defaults: applyDefaultsForUpsert preserves existing $setOnInsert values",
+
fn() {
+
const schema = z.object({
+
name: z.string(),
+
flag: z.boolean().default(true),
+
count: z.number().default(0),
+
});
+
+
const update = {
+
$set: { name: "test" },
+
$setOnInsert: { flag: false },
+
};
+
+
const result = applyDefaultsForUpsert(schema, {}, update);
+
+
assertEquals(result.$setOnInsert?.flag, false);
+
assertEquals(result.$setOnInsert?.count, 0);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name:
+
"Defaults: applyDefaultsForUpsert keeps query equality fields untouched",
+
fn() {
+
const schema = z.object({
+
status: z.string().default("pending"),
+
flag: z.boolean().default(true),
+
name: z.string(),
+
});
+
+
const query = { status: "queued" };
+
const update = { $set: { name: "upsert-test" } };
+
+
const result = applyDefaultsForUpsert(schema, query, update);
+
+
assertEquals(result.$setOnInsert?.status, undefined);
+
assertEquals(result.$setOnInsert?.flag, true);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name:
+
"Defaults: findOneAndUpdate with upsert preserves query equality fields",
+
async fn() {
+
await ProductModel.findOneAndUpdate(
+
{ name: "FindOneUpsert", category: "special" },
+
{ price: 12.5 },
+
{ upsert: true },
+
);
+
+
const product = await ProductModel.findOne({ name: "FindOneUpsert" });
+
assertExists(product);
+
+
assertEquals(product.category, "special"); // from query, not default
+
assertEquals(product.price, 12.5); // from update
+
assertEquals(product.inStock, true); // default applied
+
assertExists(product.createdAt); // default applied
+
assertEquals(product.tags, []); // default applied
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Defaults: findOneAndReplace with upsert applies defaults on creation",
+
async fn() {
+
const result = await ProductModel.findOneAndReplace(
+
{ name: "FindOneReplaceUpsert" },
+
{
+
name: "FindOneReplaceUpsert",
+
price: 77.0,
+
},
+
{ upsert: true },
+
);
+
+
assertExists(result.lastErrorObject?.upserted);
+
+
const product = await ProductModel.findOne({
+
name: "FindOneReplaceUpsert",
+
});
+
assertExists(product);
+
+
assertEquals(product.name, "FindOneReplaceUpsert");
+
assertEquals(product.price, 77.0);
+
assertEquals(product.category, "general"); // default applied
+
assertEquals(product.inStock, true); // default applied
+
assertExists(product.createdAt); // default applied
+
assertEquals(product.tags, []); // default applied
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+275
tests/errors_test.ts
···
···
+
import { assert, assertEquals, assertExists, assertRejects } from "@std/assert";
+
import {
+
connect,
+
ConnectionError,
+
disconnect,
+
Model,
+
ValidationError,
+
} from "../mod.ts";
+
import { z } from "@zod/zod";
+
import { MongoMemoryServer } from "mongodb-memory-server-core";
+
+
let mongoServer: MongoMemoryServer | null = null;
+
+
async function setupTestServer() {
+
if (!mongoServer) {
+
mongoServer = await MongoMemoryServer.create();
+
}
+
return mongoServer.getUri();
+
}
+
+
Deno.test.afterEach(async () => {
+
await disconnect();
+
});
+
+
Deno.test.afterAll(async () => {
+
if (mongoServer) {
+
await mongoServer.stop();
+
mongoServer = null;
+
}
+
});
+
+
// Test schemas
+
const userSchema = z.object({
+
name: z.string().min(1),
+
email: z.email(),
+
age: z.number().int().positive().optional(),
+
});
+
+
Deno.test({
+
name: "Errors: ValidationError - should throw on invalid insert",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
await assertRejects(
+
async () => {
+
await UserModel.insertOne({ name: "", email: "invalid" });
+
},
+
ValidationError,
+
"Validation failed on insert",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Errors: ValidationError - should have structured issues",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
try {
+
await UserModel.insertOne({ name: "", email: "invalid" });
+
throw new Error("Should have thrown ValidationError");
+
} catch (error) {
+
assert(error instanceof ValidationError);
+
assertEquals(error.operation, "insert");
+
assertExists(error.issues);
+
assert(error.issues.length > 0);
+
+
// Check field errors
+
const fieldErrors = error.getFieldErrors();
+
assertExists(fieldErrors.name);
+
assertExists(fieldErrors.email);
+
}
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Errors: ValidationError - should throw on invalid update",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
await assertRejects(
+
async () => {
+
await UserModel.updateOne({ name: "test" }, { email: "invalid-email" });
+
},
+
ValidationError,
+
"Validation failed on update",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Errors: ValidationError - should throw on invalid replace",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
// First insert a valid document
+
await UserModel.insertOne({ name: "Test", email: "test@example.com" });
+
+
await assertRejects(
+
async () => {
+
await UserModel.replaceOne({ name: "Test" }, {
+
name: "",
+
email: "invalid",
+
});
+
},
+
ValidationError,
+
"Validation failed on replace",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Errors: ValidationError - update operation should be in error",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
try {
+
await UserModel.updateOne({ name: "test" }, { age: -5 });
+
throw new Error("Should have thrown ValidationError");
+
} catch (error) {
+
assert(error instanceof ValidationError);
+
assertEquals(error.operation, "update");
+
+
const fieldErrors = error.getFieldErrors();
+
assertExists(fieldErrors.age);
+
}
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Errors: ConnectionError - should throw on connection failure",
+
async fn() {
+
await assertRejects(
+
async () => {
+
await connect(
+
"mongodb://invalid-host-that-does-not-exist:27017",
+
"test_db",
+
{
+
serverSelectionTimeoutMS: 1000, // 1 second timeout
+
connectTimeoutMS: 1000,
+
},
+
);
+
},
+
ConnectionError,
+
"Failed to connect to MongoDB",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Errors: ConnectionError - should include URI in error",
+
async fn() {
+
try {
+
await connect(
+
"mongodb://invalid-host-that-does-not-exist:27017",
+
"test_db",
+
{
+
serverSelectionTimeoutMS: 1000, // 1 second timeout
+
connectTimeoutMS: 1000,
+
},
+
);
+
throw new Error("Should have thrown ConnectionError");
+
} catch (error) {
+
assert(error instanceof ConnectionError);
+
assertEquals(
+
error.uri,
+
"mongodb://invalid-host-that-does-not-exist:27017",
+
);
+
}
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name:
+
"Errors: ConnectionError - should throw when getDb called without connection",
+
async fn() {
+
// Make sure not connected
+
await disconnect();
+
+
const { getDb } = await import("../client/connection.ts");
+
+
try {
+
getDb();
+
throw new Error("Should have thrown ConnectionError");
+
} catch (error) {
+
assert(error instanceof ConnectionError);
+
assert(error.message.includes("not connected"));
+
}
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Errors: ValidationError - field errors should be grouped correctly",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
try {
+
await UserModel.insertOne({
+
name: "",
+
email: "not-an-email",
+
age: -10,
+
});
+
throw new Error("Should have thrown ValidationError");
+
} catch (error) {
+
assert(error instanceof ValidationError);
+
+
const fieldErrors = error.getFieldErrors();
+
+
// Each field should have its own errors
+
assert(Array.isArray(fieldErrors.name));
+
assert(Array.isArray(fieldErrors.email));
+
assert(Array.isArray(fieldErrors.age));
+
+
// Verify error messages are present
+
assert(fieldErrors.name.length > 0);
+
assert(fieldErrors.email.length > 0);
+
assert(fieldErrors.age.length > 0);
+
}
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Errors: Error name should be set correctly",
+
async fn() {
+
const uri = await setupTestServer();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
try {
+
await UserModel.insertOne({ name: "", email: "invalid" });
+
} catch (error) {
+
assert(error instanceof ValidationError);
+
assertEquals(error.name, "ValidationError");
+
}
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+50
tests/features_test.ts
···
···
+
import { assert, assertExists } from "@std/assert";
+
import { ObjectId } from "mongodb";
+
import {
+
cleanupCollection,
+
createUserModel,
+
setupTestDb,
+
teardownTestDb,
+
type UserInsert,
+
type userSchema,
+
} from "./utils.ts";
+
import type { Model } from "../mod.ts";
+
+
let UserModel: Model<typeof userSchema>;
+
+
Deno.test.beforeAll(async () => {
+
await setupTestDb();
+
UserModel = createUserModel("users_features");
+
});
+
+
Deno.test.beforeEach(async () => {
+
await cleanupCollection(UserModel);
+
});
+
+
Deno.test.afterAll(async () => {
+
await teardownTestDb();
+
});
+
+
Deno.test({
+
name: "Features: Default Values - should handle default createdAt",
+
async fn() {
+
const newUser: UserInsert = {
+
name: "Default Test User",
+
email: "default@example.com",
+
// No createdAt provided - should use default
+
};
+
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
const foundUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertExists(foundUser);
+
assertExists(foundUser.createdAt);
+
assert(foundUser.createdAt instanceof Date);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+164
tests/index_test.ts
···
···
+
import { assert, assertEquals, assertExists, assertFalse } from "@std/assert";
+
import type { IndexDescription } from "mongodb";
+
import {
+
cleanupCollection,
+
createUserModel,
+
setupTestDb,
+
teardownTestDb,
+
} from "./utils.ts";
+
import type { Model } from "../mod.ts";
+
+
let UserModel: Model<typeof import("./utils.ts").userSchema>;
+
+
Deno.test.beforeAll(async () => {
+
await setupTestDb();
+
UserModel = createUserModel("users_index");
+
});
+
+
Deno.test.beforeEach(async () => {
+
await cleanupCollection(UserModel);
+
// Drop all indexes except _id
+
try {
+
await UserModel.dropIndexes();
+
} catch {
+
// Ignore if no indexes exist
+
}
+
});
+
+
Deno.test.afterAll(async () => {
+
await teardownTestDb();
+
});
+
+
Deno.test({
+
name: "Index: Create - should create a simple index",
+
async fn() {
+
const indexName = await UserModel.createIndex({ email: 1 });
+
assertExists(indexName);
+
assertEquals(typeof indexName, "string");
+
+
const indexExists = await UserModel.indexExists(indexName);
+
assert(indexExists);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Create Unique - should create a unique index",
+
async fn() {
+
const indexName = await UserModel.createIndex(
+
{ email: 1 },
+
{ unique: true, name: "email_unique_test" },
+
);
+
assertExists(indexName);
+
assertEquals(indexName, "email_unique_test");
+
+
const index = await UserModel.getIndex(indexName);
+
assertExists(index);
+
assert(index.unique);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Create Compound - should create a compound index",
+
async fn() {
+
const indexName = await UserModel.createIndex({ name: 1, age: -1 });
+
assertExists(indexName);
+
+
const index = await UserModel.getIndex(indexName);
+
assertExists(index);
+
assertEquals(Object.keys(index.key || {}).length, 2);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: List - should list all indexes",
+
async fn() {
+
// Create a few indexes
+
await UserModel.createIndex({ email: 1 });
+
await UserModel.createIndex({ name: 1, age: -1 });
+
+
const indexes = await UserModel.listIndexes();
+
// Should have at least _id index + the 2 we created
+
assert(indexes.length >= 3);
+
+
const indexNames = indexes.map((idx) => idx.name);
+
assert(indexNames.includes("_id_"));
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Drop - should drop an index",
+
async fn() {
+
const indexName = await UserModel.createIndex({ email: 1 });
+
assertExists(indexName);
+
+
let indexExists = await UserModel.indexExists(indexName);
+
assert(indexExists);
+
+
await UserModel.dropIndex(indexName);
+
+
indexExists = await UserModel.indexExists(indexName);
+
assertFalse(indexExists);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Create Multiple - should create multiple indexes",
+
async fn() {
+
const indexNames = await UserModel.createIndexes([
+
{ key: { email: 1 }, name: "email_multiple_test" },
+
{ key: { name: 1, age: -1 }, name: "name_age_multiple_test" },
+
]);
+
+
assertEquals(indexNames.length, 2);
+
assertEquals(indexNames.includes("email_multiple_test"), true);
+
assertEquals(indexNames.includes("name_age_multiple_test"), true);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Sync - should create missing indexes",
+
async fn() {
+
const indexesToSync: IndexDescription[] = [
+
{ key: { email: 1 }, name: "email_idx" },
+
{ key: { name: 1 }, name: "name_idx" },
+
];
+
+
const created = await UserModel.syncIndexes(indexesToSync);
+
assertEquals(created.length, 2);
+
+
// Running again should not create duplicates
+
const createdAgain = await UserModel.syncIndexes(indexesToSync);
+
assertEquals(createdAgain.length, 0);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Index: Get - should get index by name",
+
async fn() {
+
await UserModel.createIndex(
+
{ email: 1 },
+
{ unique: true, name: "email_unique_idx" },
+
);
+
+
const index = await UserModel.getIndex("email_unique_idx");
+
assertExists(index);
+
assertEquals(index.name, "email_unique_idx");
+
assert(index.unique);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+337
tests/transactions_test.ts
···
···
+
import { assertEquals, assertExists, assertRejects } from "@std/assert";
+
import {
+
connect,
+
disconnect,
+
endSession,
+
Model,
+
startSession,
+
withTransaction,
+
} from "../mod.ts";
+
import { z } from "@zod/zod";
+
import { MongoMemoryReplSet } from "mongodb-memory-server-core";
+
+
let replSet: MongoMemoryReplSet | null = null;
+
+
async function setupTestReplSet() {
+
if (!replSet) {
+
replSet = await MongoMemoryReplSet.create({
+
replSet: {
+
count: 1,
+
storageEngine: "wiredTiger", // Required for transactions
+
},
+
});
+
}
+
return replSet.getUri();
+
}
+
+
Deno.test.afterEach(async () => {
+
// Clean up database
+
if (replSet) {
+
try {
+
const { getDb } = await import("../client/connection.ts");
+
const db = getDb();
+
await db.dropDatabase();
+
} catch {
+
// Ignore if not connected
+
}
+
}
+
await disconnect();
+
});
+
+
Deno.test.afterAll(async () => {
+
if (replSet) {
+
await replSet.stop();
+
replSet = null;
+
}
+
});
+
+
// Test schemas
+
const userSchema = z.object({
+
name: z.string().min(1),
+
email: z.string().email(),
+
balance: z.number().nonnegative().default(0),
+
});
+
+
const orderSchema = z.object({
+
userId: z.string(),
+
amount: z.number().positive(),
+
status: z.enum(["pending", "completed", "failed"]).default("pending"),
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should commit successful operations",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
const OrderModel = new Model("orders", orderSchema);
+
+
const result = await withTransaction(async (session) => {
+
const user = await UserModel.insertOne(
+
{ name: "Alice", email: "alice@example.com", balance: 100 },
+
{ session },
+
);
+
+
const order = await OrderModel.insertOne(
+
{ userId: user.insertedId.toString(), amount: 50 },
+
{ session },
+
);
+
+
return { userId: user.insertedId, orderId: order.insertedId };
+
});
+
+
assertExists(result.userId);
+
assertExists(result.orderId);
+
+
// Verify data was committed
+
const users = await UserModel.find({});
+
const orders = await OrderModel.find({});
+
assertEquals(users.length, 1);
+
assertEquals(orders.length, 1);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should abort on error",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
await assertRejects(
+
async () => {
+
await withTransaction(async (session) => {
+
await UserModel.insertOne(
+
{ name: "Bob", email: "bob@example.com" },
+
{ session },
+
);
+
+
// This will fail and abort the transaction
+
throw new Error("Simulated error");
+
});
+
},
+
Error,
+
"Simulated error",
+
);
+
+
// Verify no data was committed
+
const users = await UserModel.find({});
+
assertEquals(users.length, 0);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should handle multiple operations",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
const result = await withTransaction(async (session) => {
+
const users = [];
+
+
for (let i = 0; i < 5; i++) {
+
const user = await UserModel.insertOne(
+
{ name: `User${i}`, email: `user${i}@example.com` },
+
{ session },
+
);
+
users.push(user.insertedId);
+
}
+
+
return users;
+
});
+
+
assertEquals(result.length, 5);
+
+
// Verify all users were created
+
const users = await UserModel.find({});
+
assertEquals(users.length, 5);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name:
+
"Transactions: withTransaction - should support read and write operations",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
// Insert initial user
+
const initialUser = await UserModel.insertOne({
+
name: "Charlie",
+
email: "charlie@example.com",
+
balance: 100,
+
});
+
+
const result = await withTransaction(async (session) => {
+
// Read
+
const user = await UserModel.findById(initialUser.insertedId, {
+
session,
+
});
+
assertExists(user);
+
+
// Update
+
await UserModel.updateOne(
+
{ _id: initialUser.insertedId },
+
{ balance: 150 },
+
{ session },
+
);
+
+
// Read again
+
const updatedUser = await UserModel.findById(initialUser.insertedId, {
+
session,
+
});
+
+
return updatedUser?.balance;
+
});
+
+
assertEquals(result, 150);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should handle validation errors",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
await assertRejects(
+
async () => {
+
await withTransaction(async (session) => {
+
// Valid insert
+
await UserModel.insertOne(
+
{ name: "Valid", email: "valid@example.com" },
+
{ session },
+
);
+
+
// Invalid insert (will throw ValidationError)
+
await UserModel.insertOne(
+
{ name: "", email: "invalid" },
+
{ session },
+
);
+
});
+
},
+
Error, // ValidationError
+
);
+
+
// Transaction should have been aborted, no data should exist
+
const users = await UserModel.find({});
+
assertEquals(users.length, 0);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name:
+
"Transactions: Manual session - should work with manual session management",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
const session = startSession();
+
+
try {
+
await session.withTransaction(async () => {
+
await UserModel.insertOne(
+
{ name: "Dave", email: "dave@example.com" },
+
{ session },
+
);
+
await UserModel.insertOne(
+
{ name: "Eve", email: "eve@example.com" },
+
{ session },
+
);
+
});
+
} finally {
+
await endSession(session);
+
}
+
+
// Verify both users were created
+
const users = await UserModel.find({});
+
assertEquals(users.length, 2);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should support delete operations",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
// Insert initial users
+
await UserModel.insertMany([
+
{ name: "User1", email: "user1@example.com" },
+
{ name: "User2", email: "user2@example.com" },
+
{ name: "User3", email: "user3@example.com" },
+
]);
+
+
await withTransaction(async (session) => {
+
// Delete one user
+
await UserModel.deleteOne({ name: "User1" }, { session });
+
+
// Delete multiple users
+
await UserModel.delete({ name: { $in: ["User2", "User3"] } }, {
+
session,
+
});
+
});
+
+
// Verify all were deleted
+
const users = await UserModel.find({});
+
assertEquals(users.length, 0);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should handle transaction options",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
const result = await withTransaction(
+
async (session) => {
+
await UserModel.insertOne(
+
{ name: "Frank", email: "frank@example.com" },
+
{ session },
+
);
+
return "success";
+
},
+
{
+
readPreference: "primary",
+
readConcern: { level: "snapshot" },
+
writeConcern: { w: "majority" },
+
},
+
);
+
+
assertEquals(result, "success");
+
+
const users = await UserModel.find({});
+
assertEquals(users.length, 1);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
-128
tests/user.test.js
···
-
"use strict";
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
-
return new (P || (P = Promise))(function (resolve, reject) {
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
-
});
-
};
-
var __generator = (this && this.__generator) || function (thisArg, body) {
-
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
-
function verb(n) { return function (v) { return step([n, v]); }; }
-
function step(op) {
-
if (f) throw new TypeError("Generator is already executing.");
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
-
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
-
if (y = 0, t) op = [op[0] & 2, t.value];
-
switch (op[0]) {
-
case 0: case 1: t = op; break;
-
case 4: _.label++; return { value: op[1], done: false };
-
case 5: _.label++; y = op[1]; op = [0]; continue;
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
-
default:
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
-
if (t[2]) _.ops.pop();
-
_.trys.pop(); continue;
-
}
-
op = body.call(thisArg, _);
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
-
}
-
};
-
Object.defineProperty(exports, "__esModule", { value: true });
-
var zod_1 = require("zod");
-
var src_1 = require("../src");
-
var mongodb_1 = require("mongodb");
-
var userSchema = (0, src_1.defineModel)(zod_1.z.object({
-
name: zod_1.z.string(),
-
email: zod_1.z.string().email(),
-
age: zod_1.z.number().int().positive().optional(),
-
createdAt: zod_1.z.date().default(function () { return new Date(); }),
-
}));
-
function runTests() {
-
return __awaiter(this, void 0, void 0, function () {
-
var UserModel, newUser, insertResult, foundUser, updateResult, updatedUser, deleteResult, deletedUser, error_1;
-
return __generator(this, function (_a) {
-
switch (_a.label) {
-
case 0:
-
_a.trys.push([0, 9, 10, 12]);
-
return [4 /*yield*/, (0, src_1.connect)('mongodb://localhost:27017', 'mizzleorm_test_db')];
-
case 1:
-
_a.sent();
-
console.log('Connected to MongoDB for testing.');
-
UserModel = new src_1.MongoModel('users', userSchema);
-
// Clean up before tests
-
return [4 /*yield*/, UserModel.delete({})];
-
case 2:
-
// Clean up before tests
-
_a.sent();
-
console.log('Cleaned up existing data.');
-
newUser = {
-
name: 'Test User',
-
email: 'test@example.com',
-
age: 25,
-
};
-
return [4 /*yield*/, UserModel.insertOne(newUser)];
-
case 3:
-
insertResult = _a.sent();
-
console.log('Test 1 (Insert): User inserted with ID:', insertResult.insertedId);
-
if (!insertResult.insertedId) {
-
throw new Error('Test 1 Failed: User not inserted.');
-
}
-
return [4 /*yield*/, UserModel.findOne({ _id: new mongodb_1.ObjectId(insertResult.insertedId) })];
-
case 4:
-
foundUser = _a.sent();
-
console.log('Test 2 (Find One): Found user:', foundUser);
-
if (!foundUser || foundUser.email !== 'test@example.com') {
-
throw new Error('Test 2 Failed: User not found or data mismatch.');
-
}
-
return [4 /*yield*/, UserModel.update({ _id: new mongodb_1.ObjectId(insertResult.insertedId) }, { age: 26 })];
-
case 5:
-
updateResult = _a.sent();
-
console.log('Test 3 (Update): Modified count:', updateResult.modifiedCount);
-
if (updateResult.modifiedCount !== 1) {
-
throw new Error('Test 3 Failed: User not updated.');
-
}
-
return [4 /*yield*/, UserModel.findOne({ _id: new mongodb_1.ObjectId(insertResult.insertedId) })];
-
case 6:
-
updatedUser = _a.sent();
-
if (!updatedUser || updatedUser.age !== 26) {
-
throw new Error('Test 3 Failed: Updated user data mismatch.');
-
}
-
return [4 /*yield*/, UserModel.delete({ _id: new mongodb_1.ObjectId(insertResult.insertedId) })];
-
case 7:
-
deleteResult = _a.sent();
-
console.log('Test 4 (Delete): Deleted count:', deleteResult.deletedCount);
-
if (deleteResult.deletedCount !== 1) {
-
throw new Error('Test 4 Failed: User not deleted.');
-
}
-
return [4 /*yield*/, UserModel.findOne({ _id: new mongodb_1.ObjectId(insertResult.insertedId) })];
-
case 8:
-
deletedUser = _a.sent();
-
if (deletedUser) {
-
throw new Error('Test 4 Failed: User still exists after deletion.');
-
}
-
console.log('\nAll tests passed successfully!');
-
return [3 /*break*/, 12];
-
case 9:
-
error_1 = _a.sent();
-
console.error('\nTests failed:', error_1);
-
process.exit(1);
-
return [3 /*break*/, 12];
-
case 10: return [4 /*yield*/, (0, src_1.disconnect)()];
-
case 11:
-
_a.sent();
-
console.log('Disconnected from MongoDB.');
-
return [7 /*endfinally*/];
-
case 12: return [2 /*return*/];
-
}
-
});
-
});
-
}
-
runTests();
···
-83
tests/user.test.ts
···
-
import { z } from 'zod';
-
import { defineModel, MongoModel, connect, disconnect, InferModel, InsertType } from '../src';
-
import { ObjectId } from 'mongodb';
-
-
const userSchema = defineModel(z.object({
-
name: z.string(),
-
email: z.string().email(),
-
age: z.number().int().positive().optional(),
-
createdAt: z.date().default(() => new Date()),
-
}));
-
-
type User = InferModel<typeof userSchema>;
-
type UserInsert = InsertType<typeof userSchema>;
-
-
async function runTests() {
-
try {
-
await connect('mongodb://localhost:27017', 'mizzleorm_test_db');
-
console.log('Connected to MongoDB for testing.');
-
-
const UserModel = new MongoModel('users', userSchema);
-
-
// Clean up before tests
-
await UserModel.delete({});
-
console.log('Cleaned up existing data.');
-
-
// Test 1: Insert a new user
-
const newUser: UserInsert = {
-
name: 'Test User',
-
email: 'test@example.com',
-
age: 25,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
console.log('Test 1 (Insert): User inserted with ID:', insertResult.insertedId);
-
if (!insertResult.insertedId) {
-
throw new Error('Test 1 Failed: User not inserted.');
-
}
-
-
// Test 2: Find the inserted user
-
const foundUser = await UserModel.findOne({ _id: new ObjectId(insertResult.insertedId) });
-
console.log('Test 2 (Find One): Found user:', foundUser);
-
if (!foundUser || foundUser.email !== 'test@example.com') {
-
throw new Error('Test 2 Failed: User not found or data mismatch.');
-
}
-
-
// Test 3: Update the user
-
const updateResult = await UserModel.update(
-
{ _id: new ObjectId(insertResult.insertedId) },
-
{ age: 26 }
-
);
-
console.log('Test 3 (Update): Modified count:', updateResult.modifiedCount);
-
if (updateResult.modifiedCount !== 1) {
-
throw new Error('Test 3 Failed: User not updated.');
-
}
-
const updatedUser = await UserModel.findOne({ _id: new ObjectId(insertResult.insertedId) });
-
if (!updatedUser || updatedUser.age !== 26) {
-
throw new Error('Test 3 Failed: Updated user data mismatch.');
-
}
-
-
// Test 4: Delete the user
-
const deleteResult = await UserModel.delete({ _id: new ObjectId(insertResult.insertedId) });
-
console.log('Test 4 (Delete): Deleted count:', deleteResult.deletedCount);
-
if (deleteResult.deletedCount !== 1) {
-
throw new Error('Test 4 Failed: User not deleted.');
-
}
-
const deletedUser = await UserModel.findOne({ _id: new ObjectId(insertResult.insertedId) });
-
if (deletedUser) {
-
throw new Error('Test 4 Failed: User still exists after deletion.');
-
}
-
-
console.log('\nAll tests passed successfully!');
-
-
} catch (error) {
-
console.error('\nTests failed:', error);
-
process.exit(1);
-
} finally {
-
await disconnect();
-
console.log('Disconnected from MongoDB.');
-
}
-
}
-
-
runTests();
-
-
···
+72
tests/utils.ts
···
···
+
import { z } from "@zod/zod";
+
import { connect, disconnect, type Input, Model } from "../mod.ts";
+
import { MongoMemoryServer } from "mongodb-memory-server-core";
+
+
export const userSchema = z.object({
+
name: z.string(),
+
email: z.email(),
+
age: z.number().int().positive().optional(),
+
createdAt: z.date().default(() => new Date()),
+
});
+
+
export type UserInsert = Input<typeof userSchema>;
+
+
let mongoServer: MongoMemoryServer | null = null;
+
let isSetup = false;
+
let setupRefCount = 0;
+
let activeDbName: string | null = null;
+
+
export async function setupTestDb(dbName = "test_db") {
+
setupRefCount++;
+
+
// If we're already connected, just share the same database
+
if (isSetup) {
+
if (activeDbName !== dbName) {
+
throw new Error(
+
`Test DB already initialized for ${activeDbName}, requested ${dbName}`,
+
);
+
}
+
return;
+
}
+
+
try {
+
mongoServer = await MongoMemoryServer.create();
+
const uri = mongoServer.getUri();
+
+
await connect(uri, dbName);
+
activeDbName = dbName;
+
isSetup = true;
+
} catch (error) {
+
// Roll back refcount if setup failed so future attempts can retry
+
setupRefCount = Math.max(0, setupRefCount - 1);
+
throw error;
+
}
+
}
+
+
export async function teardownTestDb() {
+
if (setupRefCount === 0) {
+
return;
+
}
+
+
setupRefCount = Math.max(0, setupRefCount - 1);
+
+
if (isSetup && setupRefCount === 0) {
+
await disconnect();
+
if (mongoServer) {
+
await mongoServer.stop();
+
mongoServer = null;
+
}
+
activeDbName = null;
+
isSetup = false;
+
}
+
}
+
+
export function createUserModel(
+
collectionName = "users",
+
): Model<typeof userSchema> {
+
return new Model(collectionName, userSchema);
+
}
+
+
export async function cleanupCollection(model: Model<typeof userSchema>) {
+
await model.delete({});
+
}
+165
tests/validation_test.ts
···
···
+
import { assertEquals, assertExists, assertRejects } from "@std/assert";
+
import { ObjectId } from "mongodb";
+
import {
+
cleanupCollection,
+
createUserModel,
+
setupTestDb,
+
teardownTestDb,
+
type UserInsert,
+
type userSchema,
+
} from "./utils.ts";
+
import type { Model } from "../mod.ts";
+
+
let UserModel: Model<typeof userSchema>;
+
+
Deno.test.beforeAll(async () => {
+
await setupTestDb();
+
UserModel = createUserModel("users_validation");
+
});
+
+
Deno.test.beforeEach(async () => {
+
await cleanupCollection(UserModel);
+
});
+
+
Deno.test.afterAll(async () => {
+
await teardownTestDb();
+
});
+
+
Deno.test({
+
name: "Validation: Schema - should validate user data on insert",
+
async fn() {
+
const invalidUser = {
+
name: "Invalid User",
+
email: "not-an-email", // Invalid email
+
age: -5, // Negative age
+
};
+
+
// This should throw an error due to schema validation
+
await assertRejects(
+
async () => {
+
await UserModel.insertOne(invalidUser as UserInsert);
+
},
+
Error,
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Validation: Update - should reject invalid email in update",
+
async fn() {
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Validation Test User",
+
email: "valid@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Try to update with invalid email - should throw error
+
await assertRejects(
+
async () => {
+
await UserModel.update(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ email: "not-an-email" },
+
);
+
},
+
Error,
+
"Validation failed on update",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Validation: Update - should reject negative age in update",
+
async fn() {
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Age Validation Test User",
+
email: "age@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Try to update with negative age - should throw error
+
await assertRejects(
+
async () => {
+
await UserModel.updateOne(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ age: -5 },
+
);
+
},
+
Error,
+
"Validation failed on update",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Validation: Update - should reject invalid name type in update",
+
async fn() {
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Type Validation Test User",
+
email: "type@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Try to update with invalid name type (number instead of string) - should throw error
+
await assertRejects(
+
async () => {
+
await UserModel.updateOne(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ name: 123 as unknown as string },
+
);
+
},
+
Error,
+
"Validation failed on update",
+
);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Validation: Update - should accept valid partial updates",
+
async fn() {
+
// Insert a user for this test
+
const newUser: UserInsert = {
+
name: "Valid Update Test User",
+
email: "validupdate@example.com",
+
age: 25,
+
};
+
const insertResult = await UserModel.insertOne(newUser);
+
assertExists(insertResult.insertedId);
+
+
// Update with valid data - should succeed
+
const updateResult = await UserModel.updateOne(
+
{ _id: new ObjectId(insertResult.insertedId) },
+
{ age: 30, email: "newemail@example.com" },
+
);
+
+
assertEquals(updateResult.modifiedCount, 1);
+
+
// Verify the update
+
const updatedUser = await UserModel.findOne({
+
_id: new ObjectId(insertResult.insertedId),
+
});
+
+
assertExists(updatedUser);
+
assertEquals(updatedUser.age, 30);
+
assertEquals(updatedUser.email, "newemail@example.com");
+
assertEquals(updatedUser.name, "Valid Update Test User"); // Should remain unchanged
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
-17
tsconfig.json
···
-
{
-
"compilerOptions": {
-
"target": "ES2020",
-
"module": "CommonJS",
-
"lib": ["ES2020", "DOM"],
-
"strict": true,
-
"esModuleInterop": true,
-
"skipLibCheck": true,
-
"forceConsistentCasingInFileNames": true,
-
"outDir": "./dist",
-
"declaration": true
-
},
-
"include": ["src/**/*.ts"],
-
"exclude": ["node_modules", "dist"]
-
}
-
-
···
+48
types.ts
···
···
+
import type { z } from "@zod/zod";
+
import type { Document, IndexDescription, ObjectId } from "mongodb";
+
+
/**
+
* Type alias for Zod schema objects
+
*/
+
export type Schema = z.ZodObject<z.ZodRawShape>;
+
+
/**
+
* Infer the TypeScript type from a Zod schema, including MongoDB Document
+
*/
+
export type Infer<T extends Schema> = z.infer<T> & Document;
+
+
/**
+
* Infer the model type from a Zod schema, including MongoDB Document and ObjectId
+
*/
+
export type InferModel<T extends Schema> = Infer<T> & {
+
_id?: ObjectId;
+
};
+
+
/**
+
* Infer the input type for a Zod schema (handles defaults)
+
*/
+
export type Input<T extends Schema> = z.input<T>;
+
+
/**
+
* Type for indexes that can be passed to the Model constructor
+
*/
+
export type Indexes = IndexDescription[];
+
+
/**
+
* Complete definition of a model, including schema and indexes
+
*
+
* @example
+
* ```ts
+
* const userDef: ModelDef<typeof userSchema> = {
+
* schema: userSchema,
+
* indexes: [
+
* { key: { email: 1 }, unique: true },
+
* { key: { name: 1 } }
+
* ]
+
* };
+
* ```
+
*/
+
export type ModelDef<T extends Schema> = {
+
schema: T;
+
indexes?: Indexes;
+
};