From 5d8182895c1478b4b8c9f3b53549527453e5471e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:55:06 +0000 Subject: [PATCH 01/12] Initial plan From 81150447a6a1fd2876e7207b8de4ce522ab95cf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:04:02 +0000 Subject: [PATCH 02/12] Initial plan for mongoose package creation Co-authored-by: goastler <7059456+goastler@users.noreply.github.com> --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac6600153a..9330a3aa3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@prosopo/captcha", - "version": "3.4.13", + "version": "3.4.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@prosopo/captcha", - "version": "3.4.13", + "version": "3.4.14", "license": "Apache-2.0", "workspaces": [ "dev/*", From 1d41511a427d495d108a0b0354f5bd4351545948 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:17:16 +0000 Subject: [PATCH 03/12] Create mongoose package with middleware and utilities Co-authored-by: goastler <7059456+goastler@users.noreply.github.com> --- package-lock.json | 52 ++++ packages/mongoose/CHANGELOG.md | 15 + packages/mongoose/README.md | 22 ++ packages/mongoose/package.json | 61 ++++ packages/mongoose/src/connection.ts | 133 ++++++++ packages/mongoose/src/index.ts | 28 ++ packages/mongoose/src/middleware.ts | 124 ++++++++ packages/mongoose/src/schema.ts | 109 +++++++ packages/mongoose/src/tests/mongoose.test.ts | 300 +++++++++++++++++++ packages/mongoose/src/zodMapper.ts | 228 ++++++++++++++ packages/mongoose/tsconfig.cjs.json | 13 + packages/mongoose/tsconfig.json | 19 ++ packages/mongoose/tsconfig.types.json | 9 + packages/mongoose/vite.cjs.config.ts | 22 ++ packages/mongoose/vite.esm.config.ts | 20 ++ packages/mongoose/vite.test.config.ts | 33 ++ 16 files changed, 1188 insertions(+) create mode 100644 packages/mongoose/CHANGELOG.md create mode 100644 packages/mongoose/README.md create mode 100644 packages/mongoose/package.json create mode 100644 packages/mongoose/src/connection.ts create mode 100644 packages/mongoose/src/index.ts create mode 100644 packages/mongoose/src/middleware.ts create mode 100644 packages/mongoose/src/schema.ts create mode 100644 packages/mongoose/src/tests/mongoose.test.ts create mode 100644 packages/mongoose/src/zodMapper.ts create mode 100644 packages/mongoose/tsconfig.cjs.json create mode 100644 packages/mongoose/tsconfig.json create mode 100644 packages/mongoose/tsconfig.types.json create mode 100644 packages/mongoose/vite.cjs.config.ts create mode 100644 packages/mongoose/vite.esm.config.ts create mode 100644 packages/mongoose/vite.test.config.ts diff --git a/package-lock.json b/package-lock.json index 9330a3aa3c..a89b2b9e4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18976,6 +18976,10 @@ "resolved": "packages/locale", "link": true }, + "node_modules/@prosopo/mongoose": { + "resolved": "packages/mongoose", + "link": true + }, "node_modules/@prosopo/procaptcha": { "resolved": "packages/procaptcha", "link": true @@ -47358,6 +47362,54 @@ } } }, + "packages/mongoose": { + "name": "@prosopo/mongoose", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@prosopo/common": "3.1.21", + "mongodb": "6.15.0", + "mongoose": "8.13.0", + "zod": "3.24.1" + }, + "devDependencies": { + "@prosopo/config": "3.1.21", + "@types/node": "22.10.2", + "@vitest/coverage-v8": "3.0.9", + "concurrently": "9.0.1", + "del-cli": "6.0.0", + "mongodb-memory-server": "10.0.0", + "npm-run-all": "4.1.5", + "tslib": "2.7.0", + "tsx": "4.20.3", + "typescript": "5.6.2", + "vite": "6.3.5", + "vitest": "3.0.9" + }, + "engines": { + "node": "20", + "npm": "10.8.2" + } + }, + "packages/mongoose/node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "packages/mongoose/node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/procaptcha": { "name": "@prosopo/procaptcha", "version": "2.9.10", diff --git a/packages/mongoose/CHANGELOG.md b/packages/mongoose/CHANGELOG.md new file mode 100644 index 0000000000..39bd960045 --- /dev/null +++ b/packages/mongoose/CHANGELOG.md @@ -0,0 +1,15 @@ +# @prosopo/mongoose + +## 1.0.0 + +### Major Changes + +- Initial release of the Mongoose utilities package +- Added `createMongooseConnection()` for creating MongoDB connections +- Added standard middleware for automatic timestamp management (createdAt, updatedAt) +- Added version increment middleware for all mutating operations +- Added `createSchemaWithMiddleware()` for creating schemas with middleware pre-applied +- Added `getOrCreateModel()` for safe model creation that handles multiple calls +- Added `createSchemaBuilder()` for fluent schema and model creation +- Added `createModelFromZodSchema()` for Zod-to-Mongoose schema mapping with validation +- Added comprehensive test coverage diff --git a/packages/mongoose/README.md b/packages/mongoose/README.md new file mode 100644 index 0000000000..a08a425474 --- /dev/null +++ b/packages/mongoose/README.md @@ -0,0 +1,22 @@ +# @prosopo/mongoose + +Mongoose utilities and middleware for Prosopo packages. + +This package provides: +- Mongoose connection management utilities +- Standard middleware for timestamp management (createdAt, updatedAt) +- Version increment middleware for mutation operations +- Schema and model builders with automatic middleware application +- Zod-to-Mongoose schema mapping with validation +- Model caching to support multiple `.model()` calls + +## Features + +### Automatic Middleware +All schemas created with this package automatically include: +- Version increment (`__v`) on all mutating operations +- `createdAt` timestamp (set once on creation, never overwritten) +- `updatedAt` timestamp (updated on every mutation) + +### Zod Integration +Convert Zod schemas to Mongoose schemas with automatic validation in pre and post middleware. diff --git a/packages/mongoose/package.json b/packages/mongoose/package.json new file mode 100644 index 0000000000..8315c5d46a --- /dev/null +++ b/packages/mongoose/package.json @@ -0,0 +1,61 @@ +{ + "name": "@prosopo/mongoose", + "version": "1.0.0", + "author": "PROSOPO LIMITED ", + "license": "Apache-2.0", + "private": false, + "engines": { + "node": "20", + "npm": "10.8.2" + }, + "scripts": { + "clean": "del-cli --verbose dist tsconfig.tsbuildinfo", + "build": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.esm.config.ts --mode $NODE_ENV", + "build:tsc": "tsc --build --verbose", + "build:cjs": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV", + "typecheck": "tsc --project tsconfig.types.json", + "test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts" + }, + "main": "dist/index.js", + "type": "module", + "exports": { + ".": { + "types": "dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/cjs/index.cjs" + } + }, + "types": "dist/index.d.ts", + "dependencies": { + "mongodb": "6.15.0", + "mongoose": "8.13.0", + "zod": "3.24.1" + }, + "devDependencies": { + "@prosopo/config": "3.1.21", + "@types/node": "22.10.2", + "@vitest/coverage-v8": "3.0.9", + "concurrently": "9.0.1", + "del-cli": "6.0.0", + "mongodb-memory-server": "10.0.0", + "npm-run-all": "4.1.5", + "tslib": "2.7.0", + "tsx": "4.20.3", + "typescript": "5.6.2", + "vite": "6.3.5", + "vitest": "3.0.9" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/prosopo/captcha.git" + }, + "bugs": { + "url": "https://github.com/prosopo/captcha/issues" + }, + "homepage": "https://github.com/prosopo/captcha#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org" + }, + "description": "Mongoose utilities and middleware for Prosopo packages", + "sideEffects": false +} diff --git a/packages/mongoose/src/connection.ts b/packages/mongoose/src/connection.ts new file mode 100644 index 0000000000..ce520acdd2 --- /dev/null +++ b/packages/mongoose/src/connection.ts @@ -0,0 +1,133 @@ +// Copyright 2021-2025 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ServerApiVersion } from "mongodb"; +import mongoose, { type Connection } from "mongoose"; + +mongoose.set("strictQuery", false); + +const DEFAULT_ENDPOINT = "mongodb://127.0.0.1:27017"; + +export interface MongooseConnectionOptions { + url?: string; + dbname?: string; + authSource?: string; + /** + * Optional logger object with debug and error methods + */ + logger?: { + debug: (msg: string | (() => { data?: Record; msg: string })) => void; + error: (msg: string | (() => { err?: unknown; data?: Record; msg: string })) => void; + }; +} + +/** + * Creates and manages a mongoose connection to MongoDB + * @param options Connection options + * @returns Promise that resolves to the mongoose Connection + */ +export async function createMongooseConnection( + options: MongooseConnectionOptions, +): Promise { + const baseEndpoint = options.url || DEFAULT_ENDPOINT; + const parsedUrl = new URL(baseEndpoint); + + if (options.dbname) { + parsedUrl.pathname = options.dbname; + } + if (options.authSource) { + parsedUrl.searchParams.set("authSource", options.authSource); + } + + const connectionUrl = parsedUrl.toString(); + const safeURL = connectionUrl.replace(/\w+:\w+/, ""); + const dbname = options.dbname || parsedUrl.pathname.replace("/", ""); + + if (options.logger) { + options.logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Creating mongoose connection", + })); + } + + return new Promise((resolve, reject) => { + const connection = mongoose.createConnection(connectionUrl, { + dbName: dbname, + serverApi: ServerApiVersion.v1, + }); + + const onConnected = () => { + if (options.logger) { + options.logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose connection opened", + })); + } + resolve(connection); + }; + + const onError = (err: unknown) => { + if (options.logger) { + options.logger.error(() => ({ + err, + data: { mongoUrl: safeURL }, + msg: "Mongoose connection error", + })); + } + reject(err); + }; + + connection.once("open", onConnected); + connection.once("error", onError); + + // Optional: handle other events + if (options.logger) { + connection.on("disconnected", () => { + if (options.logger) { + options.logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose disconnected", + })); + } + }); + + connection.on("reconnected", () => { + if (options.logger) { + options.logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose reconnected", + })); + } + }); + + connection.on("close", () => { + if (options.logger) { + options.logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose connection closed", + })); + } + }); + + connection.on("fullsetup", () => { + if (options.logger) { + options.logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose connection is fully setup", + })); + } + }); + } + }); +} diff --git a/packages/mongoose/src/index.ts b/packages/mongoose/src/index.ts new file mode 100644 index 0000000000..a77321a38b --- /dev/null +++ b/packages/mongoose/src/index.ts @@ -0,0 +1,28 @@ +// Copyright 2021-2025 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export { + createMongooseConnection, + type MongooseConnectionOptions, +} from "./connection.js"; +export { applyStandardMiddleware } from "./middleware.js"; +export { + createSchemaBuilder, + createSchemaWithMiddleware, + getOrCreateModel, +} from "./schema.js"; +export { + createModelFromZodSchema, + zodToMongooseSchema, +} from "./zodMapper.js"; diff --git a/packages/mongoose/src/middleware.ts b/packages/mongoose/src/middleware.ts new file mode 100644 index 0000000000..2f62272750 --- /dev/null +++ b/packages/mongoose/src/middleware.ts @@ -0,0 +1,124 @@ +// Copyright 2021-2025 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { Query, Schema } from "mongoose"; + +interface TimestampedDocument { + createdAt?: Date; + updatedAt?: Date; +} + +/** + * Adds standard middleware to a mongoose schema: + * - Increments __v on all mutating operations + * - Sets createdAt only on creation + * - Updates updatedAt on all mutations + * @param schema The mongoose schema to add middleware to + */ +export function applyStandardMiddleware(schema: Schema): void { + // Add timestamps if they don't already exist + if (!schema.path("createdAt")) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema.add({ createdAt: { type: Date } } as any); + } + if (!schema.path("updatedAt")) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema.add({ updatedAt: { type: Date } } as any); + } + + // Pre-save middleware for new documents + schema.pre("save", function (next) { + const now = new Date(); + const doc = this as unknown as TimestampedDocument; + doc.updatedAt = now; + + // Only set createdAt if this is a new document + if (this.isNew && !doc.createdAt) { + doc.createdAt = now; + } + + next(); + }); + + // Middleware for update operations + const updateMethods = [ + "updateOne", + "updateMany", + "findOneAndUpdate", + "findByIdAndUpdate", + ] as const; + + for (const method of updateMethods) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema.pre(method as any, function (this: Query, next: (err?: Error) => void) { + const update = this.getUpdate(); + + // Increment __v + if (update && typeof update === "object") { + // Handle both $set and direct updates + if ("$set" in update) { + (update.$set as Record).updatedAt = new Date(); + // Prevent createdAt from being overwritten + delete (update.$set as Record).createdAt; + } else if (!("$setOnInsert" in update)) { + (update as Record).updatedAt = new Date(); + // Prevent createdAt from being overwritten + delete (update as Record).createdAt; + } + + // Increment version + if ("$inc" in update) { + (update.$inc as Record).__v = + ((update.$inc as Record).__v || 0) + 1; + } else { + (update as Record).$inc = { __v: 1 }; + } + } + + next(); + }); + } + + // Middleware for replaceOne + schema.pre("replaceOne", function (this: Query, next) { + const replacement = this.getUpdate(); + + if (replacement && typeof replacement === "object") { + (replacement as Record).updatedAt = new Date(); + // Don't overwrite createdAt + if (!this.getOptions().upsert) { + delete (replacement as Record).createdAt; + } + } + + next(); + }); + + // Middleware for insertMany - only set createdAt, not updatedAt + schema.pre("insertMany", function (next, docs: unknown[]) { + const now = new Date(); + for (const doc of docs) { + if (doc && typeof doc === "object") { + const docObj = doc as Record; + if (!docObj.createdAt) { + docObj.createdAt = now; + } + if (!docObj.updatedAt) { + docObj.updatedAt = now; + } + } + } + next(); + }); +} diff --git a/packages/mongoose/src/schema.ts b/packages/mongoose/src/schema.ts new file mode 100644 index 0000000000..0ad7897fcf --- /dev/null +++ b/packages/mongoose/src/schema.ts @@ -0,0 +1,109 @@ +// Copyright 2021-2025 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { + Connection, + Model, + Schema, + SchemaDefinition, + SchemaOptions, +} from "mongoose"; +import { Schema as MongooseSchema } from "mongoose"; +import { applyStandardMiddleware } from "./middleware.js"; + +/** + * Creates a mongoose schema with standard middleware applied + * @param definition Schema definition + * @param options Schema options + * @returns Schema with middleware applied + */ +export function createSchemaWithMiddleware( + definition?: SchemaDefinition, + options?: SchemaOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): Schema { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const schema = new MongooseSchema(definition as any, options as any); + applyStandardMiddleware(schema); + return schema; +} + +// Cache to store models by connection and model name +const modelCache = new WeakMap>>(); + +/** + * Creates or retrieves a cached model on a connection + * This allows .model() to be called multiple times without error + * @param connection The mongoose connection + * @param modelName The name of the model + * @param schema The mongoose schema + * @returns The model + */ +export function getOrCreateModel( + connection: Connection, + modelName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: Schema, +): Model { + // Get or create the cache for this connection + let connectionCache = modelCache.get(connection); + if (!connectionCache) { + connectionCache = new Map>(); + modelCache.set(connection, connectionCache); + } + + // Check if model already exists in cache + const cachedModel = connectionCache.get(modelName); + if (cachedModel) { + return cachedModel as Model; + } + + // Check if model exists on the connection (mongoose internal cache) + if (connection.models[modelName]) { + const existingModel = connection.models[modelName] as Model; + connectionCache.set(modelName, existingModel as Model); + return existingModel; + } + + // Create new model + const model = connection.model(modelName, schema); + connectionCache.set(modelName, model as Model); + return model; +} + +/** + * Creates a mongoose schema with standard middleware and returns a model creation function + * @param definition Schema definition + * @param options Schema options + * @returns Function to create models with the schema + */ +export function createSchemaBuilder( + definition?: SchemaDefinition, + options?: SchemaOptions, +) { + const schema = createSchemaWithMiddleware(definition, options); + + return { + schema, + /** + * Creates or retrieves a model on the specified connection + * @param connection The mongoose connection + * @param modelName The name of the model + * @returns The model + */ + createModel: (connection: Connection, modelName: string): Model => { + return getOrCreateModel(connection, modelName, schema); + }, + }; +} diff --git a/packages/mongoose/src/tests/mongoose.test.ts b/packages/mongoose/src/tests/mongoose.test.ts new file mode 100644 index 0000000000..ae701ff59c --- /dev/null +++ b/packages/mongoose/src/tests/mongoose.test.ts @@ -0,0 +1,300 @@ +// Copyright 2021-2025 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { MongoMemoryServer } from "mongodb-memory-server"; +import type { Connection } from "mongoose"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { z } from "zod"; +import { + createModelFromZodSchema, + createMongooseConnection, + createSchemaBuilder, + createSchemaWithMiddleware, + getOrCreateModel, +} from "../index.js"; + +describe("Mongoose utilities", () => { + let mongoServer: MongoMemoryServer; + let connection: Connection; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const uri = mongoServer.getUri(); + connection = await createMongooseConnection({ + url: uri, + dbname: "test", + }); + }); + + afterAll(async () => { + await connection.close(); + await mongoServer.stop(); + }); + + describe("createMongooseConnection", () => { + it("should create a connection to MongoDB", async () => { + expect(connection).toBeDefined(); + expect(connection.readyState).toBe(1); // 1 = connected + }); + }); + + describe("createSchemaWithMiddleware", () => { + it("should create a schema with timestamps", () => { + interface TestDoc { + name: string; + createdAt?: Date; + updatedAt?: Date; + } + + const schema = createSchemaWithMiddleware({ + name: { type: String, required: true }, + }); + + expect(schema.path("createdAt")).toBeDefined(); + expect(schema.path("updatedAt")).toBeDefined(); + }); + + it("should set createdAt and updatedAt on save", async () => { + interface TestDoc { + name: string; + createdAt?: Date; + updatedAt?: Date; + } + + const schema = createSchemaWithMiddleware({ + name: { type: String, required: true }, + }); + const Model = connection.model("TestSave", schema); + + const doc = new Model({ name: "test" }); + await doc.save(); + + const savedDoc = doc.toObject() as TestDoc; + expect(savedDoc.createdAt).toBeInstanceOf(Date); + expect(savedDoc.updatedAt).toBeInstanceOf(Date); + }); + + it("should update updatedAt on subsequent saves", async () => { + interface TestDoc { + name: string; + createdAt?: Date; + updatedAt?: Date; + } + + const schema = createSchemaWithMiddleware({ + name: { type: String, required: true }, + }); + const Model = connection.model("TestUpdate", schema); + + const doc = new Model({ name: "test" }); + await doc.save(); + + const savedDoc = doc.toObject() as TestDoc; + const originalCreatedAt = savedDoc.createdAt; + const originalUpdatedAt = savedDoc.updatedAt; + + // Wait a bit to ensure different timestamp + await new Promise((resolve) => setTimeout(resolve, 10)); + + doc.name = "updated"; + await doc.save(); + + const updatedDoc = doc.toObject() as TestDoc; + expect(updatedDoc.createdAt).toEqual(originalCreatedAt); + expect(updatedDoc.updatedAt?.getTime()).toBeGreaterThan( + originalUpdatedAt?.getTime() || 0, + ); + }); + + it("should increment __v on update operations", async () => { + interface TestDoc { + name: string; + createdAt?: Date; + updatedAt?: Date; + } + + const schema = createSchemaWithMiddleware({ + name: { type: String, required: true }, + }); + const Model = connection.model("TestVersion", schema); + + const doc = new Model({ name: "test" }); + await doc.save(); + + const originalVersion = doc.__v; + + await Model.updateOne({ _id: doc._id }, { name: "updated" }); + + const updated = await Model.findById(doc._id); + expect(updated?.__v).toBe((originalVersion || 0) + 1); + }); + + it("should not overwrite createdAt on updates", async () => { + interface TestDoc { + name: string; + createdAt?: Date; + updatedAt?: Date; + } + + const schema = createSchemaWithMiddleware({ + name: { type: String, required: true }, + }); + const Model = connection.model("TestCreatedAtProtection", schema); + + const doc = new Model({ name: "test" }); + await doc.save(); + + const savedDoc = doc.toObject() as TestDoc; + const originalCreatedAt = savedDoc.createdAt; + + await Model.updateOne( + { _id: doc._id }, + { name: "updated", createdAt: new Date() }, + ); + + const updated = await Model.findById(doc._id); + const updatedDoc = updated?.toObject() as TestDoc; + expect(updatedDoc?.createdAt).toEqual(originalCreatedAt); + }); + }); + + describe("getOrCreateModel", () => { + it("should return the same model instance when called multiple times", () => { + const schema = createSchemaWithMiddleware({ + name: { type: String, required: true }, + }); + + const Model1 = getOrCreateModel(connection, "CachedModel", schema); + const Model2 = getOrCreateModel(connection, "CachedModel", schema); + + expect(Model1).toBe(Model2); + }); + }); + + describe("createSchemaBuilder", () => { + it("should create a schema and model factory", () => { + const builder = createSchemaBuilder({ + name: { type: String, required: true }, + }); + + expect(builder.schema).toBeDefined(); + expect(builder.createModel).toBeDefined(); + + const Model = builder.createModel(connection, "BuiltModel"); + expect(Model).toBeDefined(); + }); + }); + + describe("createModelFromZodSchema", () => { + it("should create a model from a Zod schema", () => { + const zodSchema = z.object({ + name: z.string(), + age: z.number(), + }); + + const Model = createModelFromZodSchema( + connection, + "ZodModel", + zodSchema, + ); + expect(Model).toBeDefined(); + }); + + it("should validate data with Zod on save", async () => { + const zodSchema = z.object({ + name: z.string(), + age: z.number().min(0).max(120), + }); + + const Model = createModelFromZodSchema( + connection, + "ZodValidation", + zodSchema, + ); + + // Valid data should work + const validDoc = new Model({ name: "John", age: 30 }); + await expect(validDoc.save()).resolves.toBeDefined(); + + // Invalid data should fail + const invalidDoc = new Model({ name: "Jane", age: 150 }); + await expect(invalidDoc.save()).rejects.toThrow(); + }); + + it("should validate updates with Zod", async () => { + const zodSchema = z.object({ + name: z.string().min(1), + age: z.number(), + }); + + const Model = createModelFromZodSchema( + connection, + "ZodUpdateValidation", + zodSchema, + ); + + const doc = new Model({ name: "John", age: 30 }); + await doc.save(); + + // Invalid update should fail + await expect( + Model.updateOne({ _id: doc._id }, { name: "" }), + ).rejects.toThrow(); + }); + + it("should handle optional fields in Zod schema", async () => { + const zodSchema = z.object({ + name: z.string(), + email: z.string().email().optional(), + }); + + const Model = createModelFromZodSchema( + connection, + "ZodOptional", + zodSchema, + ); + + const doc1 = new Model({ name: "John" }); + await expect(doc1.save()).resolves.toBeDefined(); + + const doc2 = new Model({ name: "Jane", email: "jane@example.com" }); + await expect(doc2.save()).resolves.toBeDefined(); + }); + + it("should include timestamp fields from middleware", async () => { + interface TestDoc { + name: string; + createdAt?: Date; + updatedAt?: Date; + } + + const zodSchema = z.object({ + name: z.string(), + }); + + const Model = createModelFromZodSchema( + connection, + "ZodTimestamps", + zodSchema, + ); + + const doc = new Model({ name: "test" }); + await doc.save(); + + const savedDoc = doc.toObject() as TestDoc; + expect(savedDoc.createdAt).toBeInstanceOf(Date); + expect(savedDoc.updatedAt).toBeInstanceOf(Date); + }); + }); +}); diff --git a/packages/mongoose/src/zodMapper.ts b/packages/mongoose/src/zodMapper.ts new file mode 100644 index 0000000000..7d21234e08 --- /dev/null +++ b/packages/mongoose/src/zodMapper.ts @@ -0,0 +1,228 @@ +// Copyright 2021-2025 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { Connection, Model, Query, Schema, SchemaDefinition } from "mongoose"; +import { z } from "zod"; +import { applyStandardMiddleware } from "./middleware.js"; +import { getOrCreateModel } from "./schema.js"; + +/** + * Converts a Zod schema to a Mongoose schema definition + * Note: This is a basic implementation that handles common types + * More complex types may need manual mapping + * @param zodSchema The Zod schema to convert + * @returns Mongoose schema definition + */ +export function zodToMongooseSchema( + zodSchema: z.ZodObject, +): SchemaDefinition { + const shape = zodSchema.shape; + const mongooseSchema: SchemaDefinition = {}; + + for (const [key, value] of Object.entries(shape)) { + mongooseSchema[key] = zodTypeToMongooseType( + value as z.ZodTypeAny, + ) as SchemaDefinition[string]; + } + + return mongooseSchema; +} + +/** + * Maps a Zod type to a Mongoose type + * @param zodType The Zod type + * @returns Mongoose type definition + */ +function zodTypeToMongooseType( + zodType: z.ZodTypeAny, +): Record { + // Handle optional and nullable types + if (zodType instanceof z.ZodOptional) { + const innerType = zodTypeToMongooseType(zodType.unwrap()); + return { + ...innerType, + required: false, + }; + } + + if (zodType instanceof z.ZodNullable) { + const innerType = zodTypeToMongooseType(zodType.unwrap()); + return { + ...innerType, + required: false, + }; + } + + if (zodType instanceof z.ZodDefault) { + const innerType = zodTypeToMongooseType(zodType.removeDefault()); + return { + ...innerType, + default: zodType._def.defaultValue(), + }; + } + + // Handle primitive types + if (zodType instanceof z.ZodString) { + return { type: String, required: true }; + } + + if (zodType instanceof z.ZodNumber) { + return { type: Number, required: true }; + } + + if (zodType instanceof z.ZodBoolean) { + return { type: Boolean, required: true }; + } + + if (zodType instanceof z.ZodDate) { + return { type: Date, required: true }; + } + + if (zodType instanceof z.ZodBigInt) { + // MongoDB doesn't natively support BigInt, use String + return { type: String, required: true }; + } + + // Handle arrays + if (zodType instanceof z.ZodArray) { + const elementType = zodTypeToMongooseType(zodType.element); + return { type: [elementType], required: true }; + } + + // Handle objects (nested schemas) + if (zodType instanceof z.ZodObject) { + return { type: zodToMongooseSchema(zodType), required: true }; + } + + // Handle enums + if (zodType instanceof z.ZodEnum) { + return { type: String, enum: zodType.options, required: true }; + } + + if (zodType instanceof z.ZodNativeEnum) { + return { type: String, enum: Object.values(zodType.enum), required: true }; + } + + // Handle unions (limited support - converts to Mixed) + if (zodType instanceof z.ZodUnion) { + return { type: Object, required: true }; + } + + // Handle any + if (zodType instanceof z.ZodAny) { + return { type: Object, required: true }; + } + + // Default to Mixed for unknown types + return { type: Object, required: true }; +} + +/** + * Creates a Mongoose model from a Zod schema with validation middleware + * @param connection The mongoose connection + * @param modelName The name of the model + * @param zodSchema The Zod schema + * @param mongooseSchema Optional pre-built mongoose schema (if you need custom mapping) + * @returns The model with Zod validation applied + */ +export function createModelFromZodSchema( + connection: Connection, + modelName: string, + zodSchema: z.ZodObject, + mongooseSchema?: Schema, +): Model> { + // Use provided schema or convert from Zod + const schema = + mongooseSchema || + new (require("mongoose").Schema)(zodToMongooseSchema(zodSchema)); + + // Apply standard middleware (timestamps, version increment) + applyStandardMiddleware(schema); + + // Add pre-save validation using Zod + schema.pre("save", async function ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this: any, + next: (err?: Error) => void, + ) { + try { + await zodSchema.parseAsync(this.toObject()); + next(); + } catch (error) { + if (error instanceof z.ZodError) { + next(new Error(`Validation failed: ${error.message}`)); + } else { + next(error as Error); + } + } + }); + + // Add pre-validation for update operations + const updateMethods = [ + "updateOne", + "updateMany", + "findOneAndUpdate", + "findByIdAndUpdate", + ] as const; + + for (const method of updateMethods) { + schema.pre( + method, + async function ( + this: Query, + next: (err?: Error) => void, + ) { + try { + const update = this.getUpdate(); + if (update && typeof update === "object") { + // Extract the actual update data (handle $set, etc.) + const updateData = + "$set" in update + ? (update.$set as Record) + : (update as Record); + + // Partial validation for updates + await zodSchema.partial().parseAsync(updateData); + } + next(); + } catch (error) { + if (error instanceof z.ZodError) { + next(new Error(`Update validation failed: ${error.message}`)); + } else { + next(error as Error); + } + } + }, + ); + } + + // Add post-find validation (optional, can be expensive) + // Uncomment if you want to validate data coming out of the database + /* + schema.post(['find', 'findOne', 'findById'], async function(docs: unknown) { + if (!docs) return; + const docArray = Array.isArray(docs) ? docs : [docs]; + for (const doc of docArray) { + try { + await zodSchema.parseAsync(doc); + } catch (error) { + console.warn('Document validation failed:', error); + } + } + }); + */ + + // Use the model cache to allow multiple calls + return getOrCreateModel(connection, modelName, schema); +} diff --git a/packages/mongoose/tsconfig.cjs.json b/packages/mongoose/tsconfig.cjs.json new file mode 100644 index 0000000000..73bd46904c --- /dev/null +++ b/packages/mongoose/tsconfig.cjs.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.cjs.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src", "src/**/*.json"], + "references": [ + { + "path": "../../dev/config/tsconfig.cjs.json" + } + ] +} diff --git a/packages/mongoose/tsconfig.json b/packages/mongoose/tsconfig.json new file mode 100644 index 0000000000..f5e764b56e --- /dev/null +++ b/packages/mongoose/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.esm.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": [ + "src", + "src/**/*.json", + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "../../dev/config/tsconfig.json" + } + ] +} diff --git a/packages/mongoose/tsconfig.types.json b/packages/mongoose/tsconfig.types.json new file mode 100644 index 0000000000..e2d2929993 --- /dev/null +++ b/packages/mongoose/tsconfig.types.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "composite": false + } +} diff --git a/packages/mongoose/vite.cjs.config.ts b/packages/mongoose/vite.cjs.config.ts new file mode 100644 index 0000000000..5c2666dc75 --- /dev/null +++ b/packages/mongoose/vite.cjs.config.ts @@ -0,0 +1,22 @@ +import path from "node:path"; +// Copyright 2021-2025 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { ViteCommonJSConfig } from "@prosopo/config"; + +export default function () { + return ViteCommonJSConfig( + path.basename("."), + path.resolve("./tsconfig.json"), + ); +} diff --git a/packages/mongoose/vite.esm.config.ts b/packages/mongoose/vite.esm.config.ts new file mode 100644 index 0000000000..4444499bfa --- /dev/null +++ b/packages/mongoose/vite.esm.config.ts @@ -0,0 +1,20 @@ +// Copyright 2021-2025 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import path from "node:path"; +import { ViteEsmConfig } from "@prosopo/config"; + +export default function () { + return ViteEsmConfig(path.basename("."), path.resolve("./tsconfig.json")); +} diff --git a/packages/mongoose/vite.test.config.ts b/packages/mongoose/vite.test.config.ts new file mode 100644 index 0000000000..d6fda1d170 --- /dev/null +++ b/packages/mongoose/vite.test.config.ts @@ -0,0 +1,33 @@ +import fs from "node:fs"; +import path from "node:path"; +// Copyright 2021-2025 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { ViteTestConfig } from "@prosopo/config"; +import dotenv from "dotenv"; +process.env.NODE_ENV = "test"; +// if .env.test exists at this level, use it, otherwise use the one at the root +const envFile = `.env.${process.env.NODE_ENV || "development"}`; +let envPath = envFile; +if (fs.existsSync(envFile)) { + envPath = path.resolve(envFile); +} else if (fs.existsSync(`../../${envFile}`)) { + envPath = path.resolve(`../../${envFile}`); +} + +// Only load dotenv if file exists +if (envPath && fs.existsSync(envPath)) { + dotenv.config({ path: envPath }); +} + +export default ViteTestConfig(); From 4c00ea6cc9bf9df8484f1a4e7434a28095361a12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:24:09 +0000 Subject: [PATCH 04/12] Update database and types-database packages to use mongoose utilities Co-authored-by: goastler <7059456+goastler@users.noreply.github.com> --- packages/database/package.json | 1 + packages/database/src/base/mongo.ts | 73 +++---------------- packages/database/src/databases/captcha.ts | 7 +- packages/database/src/databases/client.ts | 7 +- packages/database/src/databases/provider.ts | 7 +- packages/types-database/package.json | 1 + packages/types-database/src/types/provider.ts | 9 +++ 7 files changed, 39 insertions(+), 66 deletions(-) diff --git a/packages/database/package.json b/packages/database/package.json index 6964ca0675..4379211f97 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -38,6 +38,7 @@ "@prosopo/common": "3.1.21", "@prosopo/config": "3.1.21", "@prosopo/locale": "3.1.21", + "@prosopo/mongoose": "1.0.0", "@prosopo/types": "3.5.8", "@prosopo/types-database": "3.3.10", "@prosopo/user-access-policy": "3.5.24", diff --git a/packages/database/src/base/mongo.ts b/packages/database/src/base/mongo.ts index ddf2b4edbb..b6e8a718d0 100644 --- a/packages/database/src/base/mongo.ts +++ b/packages/database/src/base/mongo.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. import { type Logger, ProsopoDBError, getLogger } from "@prosopo/common"; +import { createMongooseConnection } from "@prosopo/mongoose"; import type { IDatabase } from "@prosopo/types-database"; -import { ServerApiVersion } from "mongodb"; import mongoose, { type Connection } from "mongoose"; mongoose.set("strictQuery", false); @@ -98,70 +98,17 @@ export class MongoDatabase implements IDatabase { } // Start a new connection - this.connecting = new Promise((resolve, reject) => { - const connection = mongoose.createConnection(this.url, { - dbName: this.dbname, - serverApi: ServerApiVersion.v1, + this.connecting = (async () => { + const connection = await createMongooseConnection({ + url: this.url, + dbname: this.dbname, + logger: this.logger, }); - const onConnected = () => { - this.logger.debug(() => ({ - data: { mongoUrl: this.safeURL }, - msg: "Database connection opened", - })); - this.connected = true; - this.connection = connection; - this.connecting = undefined; - resolve(); - }; - - const onError = (err: unknown) => { - this.logger.error(() => ({ - err, - data: { mongoUrl: this.safeURL }, - msg: "Database error", - })); - this.connected = false; - this.connecting = undefined; - reject(err); - }; - - connection.once("open", onConnected); - connection.once("error", onError); - - // Optional: handle other events - connection.on("disconnected", () => { - this.connected = false; - this.logger.debug(() => ({ - data: { mongoUrl: this.safeURL }, - msg: "Database disconnected", - })); - }); - - connection.on("reconnected", () => { - this.connected = true; - this.logger.debug(() => ({ - data: { mongoUrl: this.safeURL }, - msg: "Database reconnected", - })); - }); - - connection.on("close", () => { - this.connected = false; - this.logger.debug(() => ({ - data: { mongoUrl: this.safeURL }, - msg: "Database connection closed", - })); - }); - - connection.on("fullsetup", () => { - this.connected = true; - this.logger.debug(() => ({ - data: { mongoUrl: this.safeURL }, - msg: "Database connection is fully setup", - })); - }); - }); + this.connected = true; + this.connection = connection; + this.connecting = undefined; + })(); return this.connecting; } catch (e) { diff --git a/packages/database/src/databases/captcha.ts b/packages/database/src/databases/captcha.ts index 652f8b9883..a561aaa857 100644 --- a/packages/database/src/databases/captcha.ts +++ b/packages/database/src/databases/captcha.ts @@ -13,6 +13,7 @@ // limitations under the License. import { type Logger, ProsopoDBError, getLogger } from "@prosopo/common"; +import { getOrCreateModel } from "@prosopo/mongoose"; import { type CaptchaProperties, type ICaptchaDatabase, @@ -73,7 +74,11 @@ export class CaptchaDatabase extends MongoDatabase implements ICaptchaDatabase { CAPTCHA_TABLES.map(({ collectionName, modelName, schema }) => { if (this.connection) { - this.tables[collectionName] = this.connection.model(modelName, schema); + this.tables[collectionName] = getOrCreateModel( + this.connection, + modelName, + schema, + ); } }); } diff --git a/packages/database/src/databases/client.ts b/packages/database/src/databases/client.ts index 7455a8d787..486de8d40f 100644 --- a/packages/database/src/databases/client.ts +++ b/packages/database/src/databases/client.ts @@ -13,6 +13,7 @@ // limitations under the License. import { type Logger, ProsopoDBError } from "@prosopo/common"; +import { getOrCreateModel } from "@prosopo/mongoose"; import type { Timestamp } from "@prosopo/types"; import { AccountSchema, @@ -49,7 +50,11 @@ export class ClientDatabase extends MongoDatabase implements IClientDatabase { await super.connect(); CLIENT_TABLES.map(({ collectionName, modelName, schema }) => { if (this.connection) { - this.tables[collectionName] = this.connection.model(modelName, schema); + this.tables[collectionName] = getOrCreateModel( + this.connection, + modelName, + schema, + ); } }); } diff --git a/packages/database/src/databases/provider.ts b/packages/database/src/databases/provider.ts index 961f1ee31f..6da90a83c1 100644 --- a/packages/database/src/databases/provider.ts +++ b/packages/database/src/databases/provider.ts @@ -15,6 +15,7 @@ import { isHex } from "@polkadot/util/is"; import { type Logger, ProsopoDBError } from "@prosopo/common"; import type { TranslationKey } from "@prosopo/locale"; +import { getOrCreateModel } from "@prosopo/mongoose"; import { type RedisConnection, connectToRedis, @@ -239,7 +240,11 @@ export class ProviderDatabase const tables = {} as Tables; PROVIDER_TABLES.map(({ collectionName, modelName, schema }) => { if (this.connection) { - tables[collectionName] = this.connection.model(modelName, schema); + tables[collectionName] = getOrCreateModel( + this.connection, + modelName, + schema, + ); } }); this.tables = tables; diff --git a/packages/types-database/package.json b/packages/types-database/package.json index ce3faa99e4..5ec102f49a 100644 --- a/packages/types-database/package.json +++ b/packages/types-database/package.json @@ -36,6 +36,7 @@ "dependencies": { "@prosopo/common": "3.1.21", "@prosopo/locale": "3.1.21", + "@prosopo/mongoose": "1.0.0", "@prosopo/types": "3.5.8", "@prosopo/user-access-policy": "3.5.24", "@prosopo/config": "3.1.21", diff --git a/packages/types-database/src/types/provider.ts b/packages/types-database/src/types/provider.ts index 7cf6106726..02c7071a90 100644 --- a/packages/types-database/src/types/provider.ts +++ b/packages/types-database/src/types/provider.ts @@ -41,6 +41,7 @@ import { import type { AccessRulesStorage } from "@prosopo/user-access-policy"; import mongoose from "mongoose"; import { type Document, type Model, type ObjectId, Schema } from "mongoose"; +import { applyStandardMiddleware } from "@prosopo/mongoose"; import { type ZodType, any, @@ -76,6 +77,8 @@ export const ClientRecordSchema = new Schema({ settings: UserSettingsSchema, tier: { type: String, enum: Tier, required: true }, }); +// Apply standard middleware +applyStandardMiddleware(ClientRecordSchema); // Set an index on the account field, ascending ClientRecordSchema.index({ account: 1 }); @@ -224,6 +227,8 @@ export const CaptchaRecordSchema = new Schema({ required: true, }, }); +// Apply standard middleware +applyStandardMiddleware(CaptchaRecordSchema); // Set an index on the captchaId field, ascending CaptchaRecordSchema.index({ captchaId: 1 }); // Set an index on the datasetId field, ascending @@ -271,6 +276,8 @@ export const PoWCaptchaRecordSchema = new Schema({ }, coords: { type: [[[Number]]], required: false }, }); +// Apply standard middleware +applyStandardMiddleware(PoWCaptchaRecordSchema); // Set an index on the captchaId field, ascending PoWCaptchaRecordSchema.index({ challenge: 1 }); @@ -316,6 +323,8 @@ export const UserCommitmentRecordSchema = new Schema({ }, coords: { type: [[[Number]]], required: false }, }); +// Apply standard middleware +applyStandardMiddleware(UserCommitmentRecordSchema); // Set an index on the commitment id field, descending UserCommitmentRecordSchema.index({ id: -1 }); UserCommitmentRecordSchema.index({ From b3d1e32828297e06c4327ffa8b6eb0e668f92e30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:29:57 +0000 Subject: [PATCH 05/12] Apply standard middleware to all database schemas Co-authored-by: goastler <7059456+goastler@users.noreply.github.com> --- packages/types-database/src/types/captcha.ts | 7 +++++++ packages/types-database/src/types/client.ts | 3 +++ packages/types-database/src/types/provider.ts | 16 ++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/packages/types-database/src/types/captcha.ts b/packages/types-database/src/types/captcha.ts index c2ef5ed4a5..fc60420371 100644 --- a/packages/types-database/src/types/captcha.ts +++ b/packages/types-database/src/types/captcha.ts @@ -14,6 +14,7 @@ import type { PoWCaptcha } from "@prosopo/types"; import { type RootFilterQuery, Schema } from "mongoose"; +import { applyStandardMiddleware } from "@prosopo/mongoose"; import type { IDatabase } from "./mongo.js"; import { type FrictionlessTokenRecord, @@ -38,6 +39,8 @@ export const StoredSessionRecordSchema: Schema = new Schema({ ), ), }); +// Apply standard middleware +applyStandardMiddleware(StoredSessionRecordSchema); // Remove any index with 'sessionId' in its fields const existingIndexes = StoredSessionRecordSchema.indexes(); @@ -58,11 +61,15 @@ StoredSessionRecordSchema.index({ createdAt: -1 }); export const StoredUserCommitmentRecordSchema: Schema = new Schema({ ...UserCommitmentRecordSchema.obj, }); +// Apply standard middleware +applyStandardMiddleware(StoredUserCommitmentRecordSchema); StoredUserCommitmentRecordSchema.index({ frictionlessTokenId: 1 }); export const StoredPoWCaptchaRecordSchema: Schema = new Schema({ ...PoWCaptchaRecordSchema.obj, }); +// Apply standard middleware +applyStandardMiddleware(StoredPoWCaptchaRecordSchema); StoredPoWCaptchaRecordSchema.index({ frictionlessTokenId: 1 }); export interface ICaptchaDatabase extends IDatabase { diff --git a/packages/types-database/src/types/client.ts b/packages/types-database/src/types/client.ts index b31f3d11b5..ff16c11b9d 100644 --- a/packages/types-database/src/types/client.ts +++ b/packages/types-database/src/types/client.ts @@ -27,6 +27,7 @@ import { } from "@prosopo/types"; import type mongoose from "mongoose"; import { Schema } from "mongoose"; +import { applyStandardMiddleware } from "@prosopo/mongoose"; import type { IDatabase } from "./mongo.js"; import type { ClientRecord, Tables } from "./provider.js"; @@ -186,6 +187,8 @@ export const AccountSchema = new Schema({ ], deletedUsers: [], }); +// Apply standard middleware +applyStandardMiddleware(AccountSchema); export enum TableNames { accounts = "accounts", diff --git a/packages/types-database/src/types/provider.ts b/packages/types-database/src/types/provider.ts index 02c7071a90..7f657a5725 100644 --- a/packages/types-database/src/types/provider.ts +++ b/packages/types-database/src/types/provider.ts @@ -342,6 +342,8 @@ export const DatasetRecordSchema = new Schema({ format: { type: String, required: true }, solutionTree: { type: [[String]], required: true }, }); +// Apply standard middleware +applyStandardMiddleware(DatasetRecordSchema); // Set an index on the datasetId field, ascending DatasetRecordSchema.index({ datasetId: 1 }); @@ -353,6 +355,8 @@ export const SolutionRecordSchema = new Schema({ salt: { type: String, required: true }, solution: { type: [String], required: true }, }); +// Apply standard middleware +applyStandardMiddleware(SolutionRecordSchema); // Set an index on the captchaId field, ascending SolutionRecordSchema.index({ captchaId: 1 }); @@ -377,6 +381,8 @@ export const UserSolutionRecordSchema = new Schema( }, { _id: false }, ); +// Apply standard middleware +applyStandardMiddleware(UserSolutionRecordSchema); // Set an index on the captchaId field, ascending UserSolutionRecordSchema.index({ captchaId: 1 }); // Set an index on the commitment id field, descending @@ -413,6 +419,8 @@ export const PendingRecordSchema = new Schema({ }, threshold: { type: Number, required: true, default: 0.8 }, }); +// Apply standard middleware +applyStandardMiddleware(PendingRecordSchema); // Set an index on the requestHash field, descending PendingRecordSchema.index({ requestHash: -1 }); @@ -451,6 +459,8 @@ export const ScheduledTaskRecordSchema = new Schema({ required: false, }, }); +// Apply standard middleware +applyStandardMiddleware(ScheduledTaskRecordSchema); ScheduledTaskRecordSchema.index({ processName: 1 }); ScheduledTaskRecordSchema.index({ processName: 1, status: 1 }); ScheduledTaskRecordSchema.index({ _id: 1, status: 1 }); @@ -500,6 +510,8 @@ export const FrictionlessTokenRecordSchema = lastUpdatedTimestamp: { type: Date, required: false }, storedAtTimestamp: { type: Date, required: false, expires: ONE_DAY }, }); +// Apply standard middleware +applyStandardMiddleware(FrictionlessTokenRecordSchema); FrictionlessTokenRecordSchema.index({ createdAt: 1 }); FrictionlessTokenRecordSchema.index({ providerSelectEntropy: 1 }); @@ -534,6 +546,8 @@ export const SessionRecordSchema = new Schema({ webView: { type: Boolean, required: true, default: false }, iFrame: { type: Boolean, required: true, default: false }, }); +// Apply standard middleware +applyStandardMiddleware(SessionRecordSchema); SessionRecordSchema.index({ createdAt: 1 }); SessionRecordSchema.index({ deleted: 1 }); @@ -551,6 +565,8 @@ export const DetectorRecordSchema = new Schema({ detectorKey: { type: String, required: true }, expiresAt: { type: Date, required: false }, }); +// Apply standard middleware +applyStandardMiddleware(DetectorRecordSchema); DetectorRecordSchema.index({ createdAt: 1 }, { unique: true }); // TTL index for automatic cleanup of expired keys DetectorRecordSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); From 0ecf4984dfbada0532331a23086793358accc6c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:35:16 +0000 Subject: [PATCH 06/12] Fix type compatibility issues and verify builds pass Co-authored-by: goastler <7059456+goastler@users.noreply.github.com> --- package-lock.json | 3 ++- packages/database/src/databases/captcha.ts | 3 ++- packages/database/src/databases/client.ts | 3 ++- packages/database/src/databases/provider.ts | 3 ++- packages/mongoose/src/connection.ts | 5 +++-- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a89b2b9e4c..acacb309f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46834,6 +46834,7 @@ "@prosopo/common": "3.1.21", "@prosopo/config": "3.1.21", "@prosopo/locale": "3.1.21", + "@prosopo/mongoose": "1.0.0", "@prosopo/redis-client": "1.0.6", "@prosopo/types": "3.5.8", "@prosopo/types-database": "3.3.10", @@ -47367,7 +47368,6 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@prosopo/common": "3.1.21", "mongodb": "6.15.0", "mongoose": "8.13.0", "zod": "3.24.1" @@ -47914,6 +47914,7 @@ "@prosopo/common": "3.1.21", "@prosopo/config": "3.1.21", "@prosopo/locale": "3.1.21", + "@prosopo/mongoose": "1.0.0", "@prosopo/types": "3.5.8", "@prosopo/user-access-policy": "3.5.24", "mongoose": "8.13.0", diff --git a/packages/database/src/databases/captcha.ts b/packages/database/src/databases/captcha.ts index a561aaa857..13f448039c 100644 --- a/packages/database/src/databases/captcha.ts +++ b/packages/database/src/databases/captcha.ts @@ -74,10 +74,11 @@ export class CaptchaDatabase extends MongoDatabase implements ICaptchaDatabase { CAPTCHA_TABLES.map(({ collectionName, modelName, schema }) => { if (this.connection) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any this.tables[collectionName] = getOrCreateModel( this.connection, modelName, - schema, + schema as any, ); } }); diff --git a/packages/database/src/databases/client.ts b/packages/database/src/databases/client.ts index 486de8d40f..8373aba494 100644 --- a/packages/database/src/databases/client.ts +++ b/packages/database/src/databases/client.ts @@ -50,10 +50,11 @@ export class ClientDatabase extends MongoDatabase implements IClientDatabase { await super.connect(); CLIENT_TABLES.map(({ collectionName, modelName, schema }) => { if (this.connection) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any this.tables[collectionName] = getOrCreateModel( this.connection, modelName, - schema, + schema as any, ); } }); diff --git a/packages/database/src/databases/provider.ts b/packages/database/src/databases/provider.ts index 6da90a83c1..a119590074 100644 --- a/packages/database/src/databases/provider.ts +++ b/packages/database/src/databases/provider.ts @@ -240,10 +240,11 @@ export class ProviderDatabase const tables = {} as Tables; PROVIDER_TABLES.map(({ collectionName, modelName, schema }) => { if (this.connection) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any tables[collectionName] = getOrCreateModel( this.connection, modelName, - schema, + schema as any, ); } }); diff --git a/packages/mongoose/src/connection.ts b/packages/mongoose/src/connection.ts index ce520acdd2..393a2b47dc 100644 --- a/packages/mongoose/src/connection.ts +++ b/packages/mongoose/src/connection.ts @@ -25,10 +25,11 @@ export interface MongooseConnectionOptions { authSource?: string; /** * Optional logger object with debug and error methods + * Compatible with @prosopo/common Logger type */ logger?: { - debug: (msg: string | (() => { data?: Record; msg: string })) => void; - error: (msg: string | (() => { err?: unknown; data?: Record; msg: string })) => void; + debug: (fn: () => { err?: unknown; data?: Record; msg?: string }) => void; + error: (fn: () => { err?: unknown; data?: Record; msg?: string }) => void; }; } From 8a9c18e9b578cb8b4b56d48f9c1f5a06c6435447 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:30:34 +0000 Subject: [PATCH 07/12] Address PR feedback: use newSchema, singleton connection, Logger from common, mongoose overwriteModels, JSON error encoding Co-authored-by: goastler <7059456+goastler@users.noreply.github.com> --- package-lock.json | 3 +- packages/mongoose/package.json | 3 +- packages/mongoose/src/connection.ts | 131 +++++++++--------- packages/mongoose/src/index.ts | 1 + packages/mongoose/src/schema.ts | 56 ++++---- packages/mongoose/src/zodMapper.ts | 24 ++-- packages/types-database/src/types/captcha.ts | 14 +- packages/types-database/src/types/client.ts | 6 +- packages/types-database/src/types/provider.ts | 50 ++----- 9 files changed, 129 insertions(+), 159 deletions(-) diff --git a/package-lock.json b/package-lock.json index acacb309f6..0f7ffd2d7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47368,9 +47368,10 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { + "@prosopo/common": "3.1.21", "mongodb": "6.15.0", "mongoose": "8.13.0", - "zod": "3.24.1" + "zod": "3.23.8" }, "devDependencies": { "@prosopo/config": "3.1.21", diff --git a/packages/mongoose/package.json b/packages/mongoose/package.json index 8315c5d46a..745b46ccbf 100644 --- a/packages/mongoose/package.json +++ b/packages/mongoose/package.json @@ -27,9 +27,10 @@ }, "types": "dist/index.d.ts", "dependencies": { + "@prosopo/common": "3.1.21", "mongodb": "6.15.0", "mongoose": "8.13.0", - "zod": "3.24.1" + "zod": "3.23.8" }, "devDependencies": { "@prosopo/config": "3.1.21", diff --git a/packages/mongoose/src/connection.ts b/packages/mongoose/src/connection.ts index 393a2b47dc..f9a35b457d 100644 --- a/packages/mongoose/src/connection.ts +++ b/packages/mongoose/src/connection.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { type Logger, getLogger } from "@prosopo/common"; import { ServerApiVersion } from "mongodb"; import mongoose, { type Connection } from "mongoose"; @@ -19,28 +20,26 @@ mongoose.set("strictQuery", false); const DEFAULT_ENDPOINT = "mongodb://127.0.0.1:27017"; +// Singleton connection cache keyed by connection string +const connectionCache = new Map>(); + export interface MongooseConnectionOptions { url?: string; dbname?: string; authSource?: string; - /** - * Optional logger object with debug and error methods - * Compatible with @prosopo/common Logger type - */ - logger?: { - debug: (fn: () => { err?: unknown; data?: Record; msg?: string }) => void; - error: (fn: () => { err?: unknown; data?: Record; msg?: string }) => void; - }; + logger?: Logger; } /** - * Creates and manages a mongoose connection to MongoDB + * Creates and manages a singleton mongoose connection to MongoDB + * Returns the same connection instance for the same connection parameters * @param options Connection options * @returns Promise that resolves to the mongoose Connection */ export async function createMongooseConnection( options: MongooseConnectionOptions, ): Promise { + const logger = options.logger || getLogger("info", import.meta.url); const baseEndpoint = options.url || DEFAULT_ENDPOINT; const parsedUrl = new URL(baseEndpoint); @@ -55,80 +54,84 @@ export async function createMongooseConnection( const safeURL = connectionUrl.replace(/\w+:\w+/, ""); const dbname = options.dbname || parsedUrl.pathname.replace("/", ""); - if (options.logger) { - options.logger.debug(() => ({ + // Create a cache key from connection URL and dbname + const cacheKey = `${connectionUrl}::${dbname}`; + + // Return existing connection if available + if (connectionCache.has(cacheKey)) { + logger.debug(() => ({ data: { mongoUrl: safeURL }, - msg: "Creating mongoose connection", + msg: "Reusing existing mongoose connection", })); + return connectionCache.get(cacheKey)!; } - return new Promise((resolve, reject) => { + logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Creating new mongoose connection", + })); + + // Create new connection promise and cache it + const connectionPromise = new Promise((resolve, reject) => { const connection = mongoose.createConnection(connectionUrl, { dbName: dbname, serverApi: ServerApiVersion.v1, }); const onConnected = () => { - if (options.logger) { - options.logger.debug(() => ({ - data: { mongoUrl: safeURL }, - msg: "Mongoose connection opened", - })); - } + logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose connection opened", + })); resolve(connection); }; const onError = (err: unknown) => { - if (options.logger) { - options.logger.error(() => ({ - err, - data: { mongoUrl: safeURL }, - msg: "Mongoose connection error", - })); - } + logger.error(() => ({ + err, + data: { mongoUrl: safeURL }, + msg: "Mongoose connection error", + })); + // Remove from cache on error + connectionCache.delete(cacheKey); reject(err); }; connection.once("open", onConnected); connection.once("error", onError); - // Optional: handle other events - if (options.logger) { - connection.on("disconnected", () => { - if (options.logger) { - options.logger.debug(() => ({ - data: { mongoUrl: safeURL }, - msg: "Mongoose disconnected", - })); - } - }); - - connection.on("reconnected", () => { - if (options.logger) { - options.logger.debug(() => ({ - data: { mongoUrl: safeURL }, - msg: "Mongoose reconnected", - })); - } - }); - - connection.on("close", () => { - if (options.logger) { - options.logger.debug(() => ({ - data: { mongoUrl: safeURL }, - msg: "Mongoose connection closed", - })); - } - }); - - connection.on("fullsetup", () => { - if (options.logger) { - options.logger.debug(() => ({ - data: { mongoUrl: safeURL }, - msg: "Mongoose connection is fully setup", - })); - } - }); - } + // Handle other events + connection.on("disconnected", () => { + logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose disconnected", + })); + }); + + connection.on("reconnected", () => { + logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose reconnected", + })); + }); + + connection.on("close", () => { + logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose connection closed", + })); + // Remove from cache when connection closes + connectionCache.delete(cacheKey); + }); + + connection.on("fullsetup", () => { + logger.debug(() => ({ + data: { mongoUrl: safeURL }, + msg: "Mongoose connection is fully setup", + })); + }); }); + + connectionCache.set(cacheKey, connectionPromise); + return connectionPromise; } diff --git a/packages/mongoose/src/index.ts b/packages/mongoose/src/index.ts index a77321a38b..3f1a3d8bce 100644 --- a/packages/mongoose/src/index.ts +++ b/packages/mongoose/src/index.ts @@ -21,6 +21,7 @@ export { createSchemaBuilder, createSchemaWithMiddleware, getOrCreateModel, + newSchema, } from "./schema.js"; export { createModelFromZodSchema, diff --git a/packages/mongoose/src/schema.ts b/packages/mongoose/src/schema.ts index 0ad7897fcf..4bc782ae69 100644 --- a/packages/mongoose/src/schema.ts +++ b/packages/mongoose/src/schema.ts @@ -23,12 +23,13 @@ import { Schema as MongooseSchema } from "mongoose"; import { applyStandardMiddleware } from "./middleware.js"; /** - * Creates a mongoose schema with standard middleware applied + * Creates a new mongoose schema with standard middleware applied + * This is the recommended way to create schemas to ensure middleware is applied consistently * @param definition Schema definition * @param options Schema options * @returns Schema with middleware applied */ -export function createSchemaWithMiddleware( +export function newSchema( definition?: SchemaDefinition, options?: SchemaOptions, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -39,12 +40,24 @@ export function createSchemaWithMiddleware( return schema; } -// Cache to store models by connection and model name -const modelCache = new WeakMap>>(); +/** + * Creates a mongoose schema with standard middleware applied + * @deprecated Use newSchema instead + * @param definition Schema definition + * @param options Schema options + * @returns Schema with middleware applied + */ +export function createSchemaWithMiddleware( + definition?: SchemaDefinition, + options?: SchemaOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): Schema { + return newSchema(definition, options); +} /** - * Creates or retrieves a cached model on a connection - * This allows .model() to be called multiple times without error + * Creates or retrieves a model on a connection + * Uses mongoose's overwriteModels setting to allow multiple .model() calls * @param connection The mongoose connection * @param modelName The name of the model * @param schema The mongoose schema @@ -56,30 +69,11 @@ export function getOrCreateModel( // eslint-disable-next-line @typescript-eslint/no-explicit-any schema: Schema, ): Model { - // Get or create the cache for this connection - let connectionCache = modelCache.get(connection); - if (!connectionCache) { - connectionCache = new Map>(); - modelCache.set(connection, connectionCache); - } - - // Check if model already exists in cache - const cachedModel = connectionCache.get(modelName); - if (cachedModel) { - return cachedModel as Model; - } - - // Check if model exists on the connection (mongoose internal cache) - if (connection.models[modelName]) { - const existingModel = connection.models[modelName] as Model; - connectionCache.set(modelName, existingModel as Model); - return existingModel; - } - - // Create new model - const model = connection.model(modelName, schema); - connectionCache.set(modelName, model as Model); - return model; + // Enable overwriteModels to allow redefining models without errors + connection.set("overwriteModels", true); + + // Simply call model - mongoose will handle caching/overwriting + return connection.model(modelName, schema); } /** @@ -92,7 +86,7 @@ export function createSchemaBuilder( definition?: SchemaDefinition, options?: SchemaOptions, ) { - const schema = createSchemaWithMiddleware(definition, options); + const schema = newSchema(definition, options); return { schema, diff --git a/packages/mongoose/src/zodMapper.ts b/packages/mongoose/src/zodMapper.ts index 7d21234e08..bb22871ee7 100644 --- a/packages/mongoose/src/zodMapper.ts +++ b/packages/mongoose/src/zodMapper.ts @@ -13,6 +13,7 @@ // limitations under the License. import type { Connection, Model, Query, Schema, SchemaDefinition } from "mongoose"; +import { Schema as MongooseSchema } from "mongoose"; import { z } from "zod"; import { applyStandardMiddleware } from "./middleware.js"; import { getOrCreateModel } from "./schema.js"; @@ -145,7 +146,7 @@ export function createModelFromZodSchema( // Use provided schema or convert from Zod const schema = mongooseSchema || - new (require("mongoose").Schema)(zodToMongooseSchema(zodSchema)); + new MongooseSchema(zodToMongooseSchema(zodSchema)); // Apply standard middleware (timestamps, version increment) applyStandardMiddleware(schema); @@ -159,11 +160,11 @@ export function createModelFromZodSchema( try { await zodSchema.parseAsync(this.toObject()); next(); - } catch (error) { - if (error instanceof z.ZodError) { - next(new Error(`Validation failed: ${error.message}`)); + } catch (err) { + if (err instanceof z.ZodError) { + next(new Error(`Validation failed: ${JSON.stringify(err.format())}`)); } else { - next(error as Error); + next(err as Error); } } }); @@ -192,15 +193,16 @@ export function createModelFromZodSchema( ? (update.$set as Record) : (update as Record); - // Partial validation for updates - await zodSchema.partial().parseAsync(updateData); + // Partial validation for updates (allow optional fields) + const partialSchema = zodSchema.partial(); + await partialSchema.parseAsync(updateData); } next(); - } catch (error) { - if (error instanceof z.ZodError) { - next(new Error(`Update validation failed: ${error.message}`)); + } catch (err) { + if (err instanceof z.ZodError) { + next(new Error(`Update validation failed: ${JSON.stringify(err.format())}`)); } else { - next(error as Error); + next(err as Error); } } }, diff --git a/packages/types-database/src/types/captcha.ts b/packages/types-database/src/types/captcha.ts index fc60420371..830b5e8da5 100644 --- a/packages/types-database/src/types/captcha.ts +++ b/packages/types-database/src/types/captcha.ts @@ -14,7 +14,7 @@ import type { PoWCaptcha } from "@prosopo/types"; import { type RootFilterQuery, Schema } from "mongoose"; -import { applyStandardMiddleware } from "@prosopo/mongoose"; +import { newSchema } from "@prosopo/mongoose"; import type { IDatabase } from "./mongo.js"; import { type FrictionlessTokenRecord, @@ -31,7 +31,7 @@ import { export type StoredSession = SessionRecord & Omit; -export const StoredSessionRecordSchema: Schema = new Schema({ +export const StoredSessionRecordSchema: Schema = newSchema({ ...SessionRecordSchema.obj, ...Object.fromEntries( Object.entries(FrictionlessTokenRecordSchema.obj).filter( @@ -39,8 +39,6 @@ export const StoredSessionRecordSchema: Schema = new Schema({ ), ), }); -// Apply standard middleware -applyStandardMiddleware(StoredSessionRecordSchema); // Remove any index with 'sessionId' in its fields const existingIndexes = StoredSessionRecordSchema.indexes(); @@ -58,18 +56,14 @@ StoredSessionRecordSchema.index({ sessionId: 1 }, { unique: false }); // Redefine the index for createdAt without a TTL StoredSessionRecordSchema.index({ createdAt: -1 }); -export const StoredUserCommitmentRecordSchema: Schema = new Schema({ +export const StoredUserCommitmentRecordSchema: Schema = newSchema({ ...UserCommitmentRecordSchema.obj, }); -// Apply standard middleware -applyStandardMiddleware(StoredUserCommitmentRecordSchema); StoredUserCommitmentRecordSchema.index({ frictionlessTokenId: 1 }); -export const StoredPoWCaptchaRecordSchema: Schema = new Schema({ +export const StoredPoWCaptchaRecordSchema: Schema = newSchema({ ...PoWCaptchaRecordSchema.obj, }); -// Apply standard middleware -applyStandardMiddleware(StoredPoWCaptchaRecordSchema); StoredPoWCaptchaRecordSchema.index({ frictionlessTokenId: 1 }); export interface ICaptchaDatabase extends IDatabase { diff --git a/packages/types-database/src/types/client.ts b/packages/types-database/src/types/client.ts index ff16c11b9d..dd75cce676 100644 --- a/packages/types-database/src/types/client.ts +++ b/packages/types-database/src/types/client.ts @@ -27,7 +27,7 @@ import { } from "@prosopo/types"; import type mongoose from "mongoose"; import { Schema } from "mongoose"; -import { applyStandardMiddleware } from "@prosopo/mongoose"; +import { newSchema } from "@prosopo/mongoose"; import type { IDatabase } from "./mongo.js"; import type { ClientRecord, Tables } from "./provider.js"; @@ -151,7 +151,7 @@ type AccountRecord = mongoose.Document & { }; // Account format -export const AccountSchema = new Schema({ +export const AccountSchema = newSchema({ createdAt: Number, updatedAt: Number, signupEmail: String, @@ -187,8 +187,6 @@ export const AccountSchema = new Schema({ ], deletedUsers: [], }); -// Apply standard middleware -applyStandardMiddleware(AccountSchema); export enum TableNames { accounts = "accounts", diff --git a/packages/types-database/src/types/provider.ts b/packages/types-database/src/types/provider.ts index 7f657a5725..50f0840fb9 100644 --- a/packages/types-database/src/types/provider.ts +++ b/packages/types-database/src/types/provider.ts @@ -41,7 +41,7 @@ import { import type { AccessRulesStorage } from "@prosopo/user-access-policy"; import mongoose from "mongoose"; import { type Document, type Model, type ObjectId, Schema } from "mongoose"; -import { applyStandardMiddleware } from "@prosopo/mongoose"; +import { newSchema } from "@prosopo/mongoose"; import { type ZodType, any, @@ -72,13 +72,11 @@ const ONE_WEEK = ONE_DAY * 7; const ONE_MONTH = ONE_WEEK * 4; const TEN_MINUTES = 10 * 60; -export const ClientRecordSchema = new Schema({ +export const ClientRecordSchema = newSchema({ account: String, settings: UserSettingsSchema, tier: { type: String, enum: Tier, required: true }, }); -// Apply standard middleware -applyStandardMiddleware(ClientRecordSchema); // Set an index on the account field, ascending ClientRecordSchema.index({ account: 1 }); @@ -204,7 +202,7 @@ export type Tables = { [key in E]: typeof Model; }; -export const CaptchaRecordSchema = new Schema({ +export const CaptchaRecordSchema = newSchema({ captchaId: { type: String, required: true }, captchaContentId: { type: String, required: true }, assetURI: { type: String, required: false }, @@ -227,8 +225,6 @@ export const CaptchaRecordSchema = new Schema({ required: true, }, }); -// Apply standard middleware -applyStandardMiddleware(CaptchaRecordSchema); // Set an index on the captchaId field, ascending CaptchaRecordSchema.index({ captchaId: 1 }); // Set an index on the datasetId field, ascending @@ -240,7 +236,7 @@ export type PoWCaptchaRecord = mongoose.Document & PoWCaptchaStored; export type UserCommitmentRecord = mongoose.Document & UserCommitment; -export const PoWCaptchaRecordSchema = new Schema({ +export const PoWCaptchaRecordSchema = newSchema({ challenge: { type: String, required: true }, dappAccount: { type: String, required: true }, userAccount: { type: String, required: true }, @@ -276,8 +272,6 @@ export const PoWCaptchaRecordSchema = new Schema({ }, coords: { type: [[[Number]]], required: false }, }); -// Apply standard middleware -applyStandardMiddleware(PoWCaptchaRecordSchema); // Set an index on the captchaId field, ascending PoWCaptchaRecordSchema.index({ challenge: 1 }); @@ -286,7 +280,7 @@ PoWCaptchaRecordSchema.index({ dappAccount: 1, requestedAtTimestamp: 1 }); PoWCaptchaRecordSchema.index({ "ipAddress.lower": 1 }); PoWCaptchaRecordSchema.index({ "ipAddress.upper": 1 }); -export const UserCommitmentRecordSchema = new Schema({ +export const UserCommitmentRecordSchema = newSchema({ userAccount: { type: String, required: true }, dappAccount: { type: String, required: true }, providerAccount: { type: String, required: true }, @@ -323,8 +317,6 @@ export const UserCommitmentRecordSchema = new Schema({ }, coords: { type: [[[Number]]], required: false }, }); -// Apply standard middleware -applyStandardMiddleware(UserCommitmentRecordSchema); // Set an index on the commitment id field, descending UserCommitmentRecordSchema.index({ id: -1 }); UserCommitmentRecordSchema.index({ @@ -335,19 +327,17 @@ UserCommitmentRecordSchema.index({ userAccount: 1, dappAccount: 1 }); UserCommitmentRecordSchema.index({ "ipAddress.lower": 1 }); UserCommitmentRecordSchema.index({ "ipAddress.upper": 1 }); -export const DatasetRecordSchema = new Schema({ +export const DatasetRecordSchema = newSchema({ contentTree: { type: [[String]], required: true }, datasetContentId: { type: String, required: true }, datasetId: { type: String, required: true }, format: { type: String, required: true }, solutionTree: { type: [[String]], required: true }, }); -// Apply standard middleware -applyStandardMiddleware(DatasetRecordSchema); // Set an index on the datasetId field, ascending DatasetRecordSchema.index({ datasetId: 1 }); -export const SolutionRecordSchema = new Schema({ +export const SolutionRecordSchema = newSchema({ captchaId: { type: String, required: true }, captchaContentId: { type: String, required: true }, datasetId: { type: String, required: true }, @@ -355,8 +345,6 @@ export const SolutionRecordSchema = new Schema({ salt: { type: String, required: true }, solution: { type: [String], required: true }, }); -// Apply standard middleware -applyStandardMiddleware(SolutionRecordSchema); // Set an index on the captchaId field, ascending SolutionRecordSchema.index({ captchaId: 1 }); @@ -368,7 +356,7 @@ export const UserSolutionSchema = CaptchaSolutionSchema.extend({ }); export type UserSolutionRecord = mongoose.Document & zInfer; -export const UserSolutionRecordSchema = new Schema( +export const UserSolutionRecordSchema = newSchema( { captchaId: { type: String, required: true }, captchaContentId: { type: String, required: true }, @@ -381,8 +369,6 @@ export const UserSolutionRecordSchema = new Schema( }, { _id: false }, ); -// Apply standard middleware -applyStandardMiddleware(UserSolutionRecordSchema); // Set an index on the captchaId field, ascending UserSolutionRecordSchema.index({ captchaId: 1 }); // Set an index on the commitment id field, descending @@ -405,7 +391,7 @@ export type PendingCaptchaRequestMongoose = Omit< export type FrictionlessTokenId = mongoose.Schema.Types.ObjectId; -export const PendingRecordSchema = new Schema({ +export const PendingRecordSchema = newSchema({ accountId: { type: String, required: true }, pending: { type: Boolean, required: true }, salt: { type: String, required: true }, @@ -419,8 +405,6 @@ export const PendingRecordSchema = new Schema({ }, threshold: { type: Number, required: true, default: 0.8 }, }); -// Apply standard middleware -applyStandardMiddleware(PendingRecordSchema); // Set an index on the requestHash field, descending PendingRecordSchema.index({ requestHash: -1 }); @@ -443,7 +427,7 @@ type ScheduledTaskMongoose = Omit & { datetime: Date; }; -export const ScheduledTaskRecordSchema = new Schema({ +export const ScheduledTaskRecordSchema = newSchema({ processName: { type: String, enum: ScheduledTaskNames, required: true }, datetime: { type: Date, required: true, expires: ONE_WEEK }, updated: { type: Number, required: false }, @@ -459,8 +443,6 @@ export const ScheduledTaskRecordSchema = new Schema({ required: false, }, }); -// Apply standard middleware -applyStandardMiddleware(ScheduledTaskRecordSchema); ScheduledTaskRecordSchema.index({ processName: 1 }); ScheduledTaskRecordSchema.index({ processName: 1, status: 1 }); ScheduledTaskRecordSchema.index({ _id: 1, status: 1 }); @@ -492,7 +474,7 @@ type FrictionlessTokenMongoose = FrictionlessTokenRecord & { }; export const FrictionlessTokenRecordSchema = - new Schema({ + newSchema({ token: { type: String, required: true, unique: true }, score: { type: Number, required: true }, threshold: { type: Number, required: true }, @@ -510,8 +492,6 @@ export const FrictionlessTokenRecordSchema = lastUpdatedTimestamp: { type: Date, required: false }, storedAtTimestamp: { type: Date, required: false, expires: ONE_DAY }, }); -// Apply standard middleware -applyStandardMiddleware(FrictionlessTokenRecordSchema); FrictionlessTokenRecordSchema.index({ createdAt: 1 }); FrictionlessTokenRecordSchema.index({ providerSelectEntropy: 1 }); @@ -531,7 +511,7 @@ export type Session = { export type SessionRecord = mongoose.Document & Session; -export const SessionRecordSchema = new Schema({ +export const SessionRecordSchema = newSchema({ sessionId: { type: String, required: true }, createdAt: { type: Date, required: true }, tokenId: { @@ -546,8 +526,6 @@ export const SessionRecordSchema = new Schema({ webView: { type: Boolean, required: true, default: false }, iFrame: { type: Boolean, required: true, default: false }, }); -// Apply standard middleware -applyStandardMiddleware(SessionRecordSchema); SessionRecordSchema.index({ createdAt: 1 }); SessionRecordSchema.index({ deleted: 1 }); @@ -560,13 +538,11 @@ export type DetectorKey = { }; export type DetectorSchema = mongoose.Document & DetectorKey; -export const DetectorRecordSchema = new Schema({ +export const DetectorRecordSchema = newSchema({ createdAt: { type: Date, required: true }, detectorKey: { type: String, required: true }, expiresAt: { type: Date, required: false }, }); -// Apply standard middleware -applyStandardMiddleware(DetectorRecordSchema); DetectorRecordSchema.index({ createdAt: 1 }, { unique: true }); // TTL index for automatic cleanup of expired keys DetectorRecordSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); From 2c5bda0e8c995b78d57db4ee7d610c311aac6c16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:35:37 +0000 Subject: [PATCH 08/12] Add socket timeout, heartbeat frequency, wait for connected event, remove singleton, add validation middleware Co-authored-by: goastler <7059456+goastler@users.noreply.github.com> --- packages/mongoose/src/connection.ts | 36 +++++++---------------------- packages/mongoose/src/middleware.ts | 21 +++++++++++++++-- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/mongoose/src/connection.ts b/packages/mongoose/src/connection.ts index f9a35b457d..d6204dc30b 100644 --- a/packages/mongoose/src/connection.ts +++ b/packages/mongoose/src/connection.ts @@ -20,9 +20,6 @@ mongoose.set("strictQuery", false); const DEFAULT_ENDPOINT = "mongodb://127.0.0.1:27017"; -// Singleton connection cache keyed by connection string -const connectionCache = new Map>(); - export interface MongooseConnectionOptions { url?: string; dbname?: string; @@ -31,8 +28,7 @@ export interface MongooseConnectionOptions { } /** - * Creates and manages a singleton mongoose connection to MongoDB - * Returns the same connection instance for the same connection parameters + * Creates and manages a mongoose connection to MongoDB * @param options Connection options * @returns Promise that resolves to the mongoose Connection */ @@ -54,34 +50,24 @@ export async function createMongooseConnection( const safeURL = connectionUrl.replace(/\w+:\w+/, ""); const dbname = options.dbname || parsedUrl.pathname.replace("/", ""); - // Create a cache key from connection URL and dbname - const cacheKey = `${connectionUrl}::${dbname}`; - - // Return existing connection if available - if (connectionCache.has(cacheKey)) { - logger.debug(() => ({ - data: { mongoUrl: safeURL }, - msg: "Reusing existing mongoose connection", - })); - return connectionCache.get(cacheKey)!; - } - logger.debug(() => ({ data: { mongoUrl: safeURL }, msg: "Creating new mongoose connection", })); - // Create new connection promise and cache it - const connectionPromise = new Promise((resolve, reject) => { + // Create new connection promise + return new Promise((resolve, reject) => { const connection = mongoose.createConnection(connectionUrl, { dbName: dbname, serverApi: ServerApiVersion.v1, + socketTimeoutMS: 30000, // 30 seconds + heartbeatFrequencyMS: 10000, // 10 seconds }); const onConnected = () => { logger.debug(() => ({ data: { mongoUrl: safeURL }, - msg: "Mongoose connection opened", + msg: "Mongoose connection connected", })); resolve(connection); }; @@ -92,12 +78,11 @@ export async function createMongooseConnection( data: { mongoUrl: safeURL }, msg: "Mongoose connection error", })); - // Remove from cache on error - connectionCache.delete(cacheKey); reject(err); }; - connection.once("open", onConnected); + // Wait for 'connected' event instead of 'open' + connection.once("connected", onConnected); connection.once("error", onError); // Handle other events @@ -120,8 +105,6 @@ export async function createMongooseConnection( data: { mongoUrl: safeURL }, msg: "Mongoose connection closed", })); - // Remove from cache when connection closes - connectionCache.delete(cacheKey); }); connection.on("fullsetup", () => { @@ -131,7 +114,4 @@ export async function createMongooseConnection( })); }); }); - - connectionCache.set(cacheKey, connectionPromise); - return connectionPromise; } diff --git a/packages/mongoose/src/middleware.ts b/packages/mongoose/src/middleware.ts index 2f62272750..b651917437 100644 --- a/packages/mongoose/src/middleware.ts +++ b/packages/mongoose/src/middleware.ts @@ -90,8 +90,25 @@ export function applyStandardMiddleware(schema: Schema): void { }); } + // Add validation middleware for update operations + for (const method of updateMethods) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema.pre(method as any, function (this: Query, next: (err?: Error) => void) { + // Set runValidators to true to ensure validation runs on updates + this.setOptions({ runValidators: true }); + next(); + }); + } + + // Also ensure validation runs on update() method + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema.pre("update" as any, function (this: Query, next: (err?: Error) => void) { + this.setOptions({ runValidators: true }); + next(); + }); + // Middleware for replaceOne - schema.pre("replaceOne", function (this: Query, next) { + schema.pre("replaceOne", function (this: Query, next: (err?: Error) => void) { const replacement = this.getUpdate(); if (replacement && typeof replacement === "object") { @@ -106,7 +123,7 @@ export function applyStandardMiddleware(schema: Schema): void { }); // Middleware for insertMany - only set createdAt, not updatedAt - schema.pre("insertMany", function (next, docs: unknown[]) { + schema.pre("insertMany", function (next: (err?: Error) => void, docs: unknown[]) { const now = new Date(); for (const doc of docs) { if (doc && typeof doc === "object") { From 658e94b6cfa4a5122d5fa0b671288807a5fd36a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:40:49 +0000 Subject: [PATCH 09/12] Convert middleware to Mongoose plugin pattern Co-authored-by: goastler <7059456+goastler@users.noreply.github.com> --- packages/mongoose/README.md | 41 ++++++++++++++++++++++++++--- packages/mongoose/src/index.ts | 2 +- packages/mongoose/src/middleware.ts | 16 +++++++++-- packages/mongoose/src/schema.ts | 9 ++++--- packages/mongoose/src/zodMapper.ts | 6 ++--- 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/packages/mongoose/README.md b/packages/mongoose/README.md index a08a425474..450b11ba25 100644 --- a/packages/mongoose/README.md +++ b/packages/mongoose/README.md @@ -4,19 +4,52 @@ Mongoose utilities and middleware for Prosopo packages. This package provides: - Mongoose connection management utilities -- Standard middleware for timestamp management (createdAt, updatedAt) +- Standard middleware plugin for timestamp management (createdAt, updatedAt) - Version increment middleware for mutation operations -- Schema and model builders with automatic middleware application +- Schema and model builders with automatic middleware application via plugin - Zod-to-Mongoose schema mapping with validation - Model caching to support multiple `.model()` calls ## Features -### Automatic Middleware -All schemas created with this package automatically include: +### Automatic Middleware Plugin +All schemas created with this package automatically include the standard middleware plugin: - Version increment (`__v`) on all mutating operations - `createdAt` timestamp (set once on creation, never overwritten) - `updatedAt` timestamp (updated on every mutation) +- Validation enabled on all update operations + +The middleware is applied as a Mongoose plugin, which is the recommended approach for extending schema functionality. ### Zod Integration Convert Zod schemas to Mongoose schemas with automatic validation in pre and post middleware. + +## Usage + +### Creating Schemas with newSchema() + +```typescript +import { newSchema } from '@prosopo/mongoose'; + +// The standard middleware plugin is automatically applied +const UserSchema = newSchema({ + name: { type: String, required: true }, + email: { type: String, required: true } +}); +``` + +### Using the Plugin Directly + +You can also apply the standard middleware plugin to existing schemas: + +```typescript +import { Schema } from 'mongoose'; +import { standardMiddlewarePlugin } from '@prosopo/mongoose'; + +const MySchema = new Schema({ + field: String +}); + +// Apply the plugin +MySchema.plugin(standardMiddlewarePlugin); +``` diff --git a/packages/mongoose/src/index.ts b/packages/mongoose/src/index.ts index 3f1a3d8bce..6115d477d4 100644 --- a/packages/mongoose/src/index.ts +++ b/packages/mongoose/src/index.ts @@ -16,7 +16,7 @@ export { createMongooseConnection, type MongooseConnectionOptions, } from "./connection.js"; -export { applyStandardMiddleware } from "./middleware.js"; +export { applyStandardMiddleware, standardMiddlewarePlugin } from "./middleware.js"; export { createSchemaBuilder, createSchemaWithMiddleware, diff --git a/packages/mongoose/src/middleware.ts b/packages/mongoose/src/middleware.ts index b651917437..31afaefc69 100644 --- a/packages/mongoose/src/middleware.ts +++ b/packages/mongoose/src/middleware.ts @@ -20,13 +20,16 @@ interface TimestampedDocument { } /** - * Adds standard middleware to a mongoose schema: + * Mongoose plugin that adds standard middleware to a schema: * - Increments __v on all mutating operations * - Sets createdAt only on creation * - Updates updatedAt on all mutations + * - Ensures validation runs on update operations + * * @param schema The mongoose schema to add middleware to + * @param options Optional plugin options (currently unused) */ -export function applyStandardMiddleware(schema: Schema): void { +export function standardMiddlewarePlugin(schema: Schema, options?: unknown): void { // Add timestamps if they don't already exist if (!schema.path("createdAt")) { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -139,3 +142,12 @@ export function applyStandardMiddleware(schema: Schema): void { next(); }); } + +/** + * Legacy function that applies the standard middleware plugin to a schema + * @deprecated Use schema.plugin(standardMiddlewarePlugin) instead + * @param schema The mongoose schema to add middleware to + */ +export function applyStandardMiddleware(schema: Schema): void { + standardMiddlewarePlugin(schema); +} diff --git a/packages/mongoose/src/schema.ts b/packages/mongoose/src/schema.ts index 4bc782ae69..aaa0acf3e3 100644 --- a/packages/mongoose/src/schema.ts +++ b/packages/mongoose/src/schema.ts @@ -20,14 +20,14 @@ import type { SchemaOptions, } from "mongoose"; import { Schema as MongooseSchema } from "mongoose"; -import { applyStandardMiddleware } from "./middleware.js"; +import { standardMiddlewarePlugin } from "./middleware.js"; /** - * Creates a new mongoose schema with standard middleware applied + * Creates a new mongoose schema with standard middleware plugin applied * This is the recommended way to create schemas to ensure middleware is applied consistently * @param definition Schema definition * @param options Schema options - * @returns Schema with middleware applied + * @returns Schema with middleware applied via plugin */ export function newSchema( definition?: SchemaDefinition, @@ -36,7 +36,8 @@ export function newSchema( ): Schema { // eslint-disable-next-line @typescript-eslint/no-explicit-any const schema = new MongooseSchema(definition as any, options as any); - applyStandardMiddleware(schema); + // Apply standard middleware via plugin + schema.plugin(standardMiddlewarePlugin); return schema; } diff --git a/packages/mongoose/src/zodMapper.ts b/packages/mongoose/src/zodMapper.ts index bb22871ee7..2ce6db0da4 100644 --- a/packages/mongoose/src/zodMapper.ts +++ b/packages/mongoose/src/zodMapper.ts @@ -15,7 +15,7 @@ import type { Connection, Model, Query, Schema, SchemaDefinition } from "mongoose"; import { Schema as MongooseSchema } from "mongoose"; import { z } from "zod"; -import { applyStandardMiddleware } from "./middleware.js"; +import { standardMiddlewarePlugin } from "./middleware.js"; import { getOrCreateModel } from "./schema.js"; /** @@ -148,8 +148,8 @@ export function createModelFromZodSchema( mongooseSchema || new MongooseSchema(zodToMongooseSchema(zodSchema)); - // Apply standard middleware (timestamps, version increment) - applyStandardMiddleware(schema); + // Apply standard middleware via plugin (timestamps, version increment) + schema.plugin(standardMiddlewarePlugin); // Add pre-save validation using Zod schema.pre("save", async function ( From 28bf71a806da9f47bb0cfa66a0ef819a9992db7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:44:35 +0000 Subject: [PATCH 10/12] Enable timestamps option by default in newSchema and createModelFromZodSchema Co-authored-by: goastler <7059456+goastler@users.noreply.github.com> --- packages/mongoose/src/schema.ts | 10 ++++++++-- packages/mongoose/src/zodMapper.ts | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/mongoose/src/schema.ts b/packages/mongoose/src/schema.ts index aaa0acf3e3..12a46e5953 100644 --- a/packages/mongoose/src/schema.ts +++ b/packages/mongoose/src/schema.ts @@ -26,7 +26,7 @@ import { standardMiddlewarePlugin } from "./middleware.js"; * Creates a new mongoose schema with standard middleware plugin applied * This is the recommended way to create schemas to ensure middleware is applied consistently * @param definition Schema definition - * @param options Schema options + * @param options Schema options (timestamps will be set to true if not explicitly provided) * @returns Schema with middleware applied via plugin */ export function newSchema( @@ -34,8 +34,14 @@ export function newSchema( options?: SchemaOptions, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Schema { + // Merge options with timestamps enabled by default + const schemaOptions: SchemaOptions = { + ...options, + timestamps: options?.timestamps !== undefined ? options.timestamps : true, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any - const schema = new MongooseSchema(definition as any, options as any); + const schema = new MongooseSchema(definition as any, schemaOptions as any); // Apply standard middleware via plugin schema.plugin(standardMiddlewarePlugin); return schema; diff --git a/packages/mongoose/src/zodMapper.ts b/packages/mongoose/src/zodMapper.ts index 2ce6db0da4..ea2a9b8b78 100644 --- a/packages/mongoose/src/zodMapper.ts +++ b/packages/mongoose/src/zodMapper.ts @@ -143,10 +143,10 @@ export function createModelFromZodSchema( zodSchema: z.ZodObject, mongooseSchema?: Schema, ): Model> { - // Use provided schema or convert from Zod + // Use provided schema or convert from Zod with timestamps enabled const schema = mongooseSchema || - new MongooseSchema(zodToMongooseSchema(zodSchema)); + new MongooseSchema(zodToMongooseSchema(zodSchema), { timestamps: true }); // Apply standard middleware via plugin (timestamps, version increment) schema.plugin(standardMiddlewarePlugin); From fd09e6fe26fb2325d6e2334909a9ace48416fd5d Mon Sep 17 00:00:00 2001 From: Hugh Date: Wed, 12 Nov 2025 12:35:57 +0000 Subject: [PATCH 11/12] fix: update mongoose package dependencies to match current versions - Updated @prosopo/common from 3.1.21 to 3.1.23 - Updated @prosopo/config from 3.1.21 to 3.1.23 - Resolved changesets validation errors --- package-lock.json | 78 +++++++++++++--------------------- packages/mongoose/package.json | 4 +- 2 files changed, 32 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4abbe60bb3..3a7f0f91af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30815,7 +30815,7 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -33623,13 +33623,13 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@prosopo/common": "3.1.21", + "@prosopo/common": "3.1.23", "mongodb": "6.15.0", "mongoose": "8.13.0", "zod": "3.23.8" }, "devDependencies": { - "@prosopo/config": "3.1.21", + "@prosopo/config": "3.1.23", "@types/node": "22.10.2", "@vitest/coverage-v8": "3.0.9", "concurrently": "9.0.1", @@ -33654,6 +33654,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33670,6 +33671,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33686,6 +33688,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33702,6 +33705,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33718,6 +33722,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33734,6 +33739,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33750,6 +33756,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33766,6 +33773,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33782,6 +33790,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33798,6 +33807,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33814,6 +33824,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33830,6 +33841,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33846,6 +33858,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33862,6 +33875,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33878,6 +33892,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33894,6 +33909,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33910,6 +33926,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33926,6 +33943,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33942,6 +33960,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33958,6 +33977,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33974,6 +33994,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -33990,6 +34011,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -34006,6 +34028,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -34022,6 +34045,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -34038,6 +34062,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -34054,6 +34079,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -34063,50 +34089,6 @@ "node": ">=18" } }, - "packages/mongoose/node_modules/@prosopo/common": { - "version": "3.1.21", - "resolved": "https://registry.npmjs.org/@prosopo/common/-/common-3.1.21.tgz", - "integrity": "sha512-EZvnRy+Sg6jB620yfuCITkC6C23kthuP8u/j9/rUfwq/2U5OEoHFFBOo96kO7XmIG3Ow82JqjmxrW5trUs8jgQ==", - "license": "Apache-2.0", - "dependencies": { - "@prosopo/config": "3.1.21", - "@prosopo/locale": "3.1.21", - "i18next": "24.1.0", - "zod": "3.23.8" - }, - "engines": { - "node": ">=v20.0.0", - "npm": ">=10.6.0" - } - }, - "packages/mongoose/node_modules/@prosopo/config": { - "version": "3.1.21", - "resolved": "https://registry.npmjs.org/@prosopo/config/-/config-3.1.21.tgz", - "integrity": "sha512-g2SASnWZixsjMta2CU1lNRwG1DQBjZKevdU4/vg0fNVS3Rms64DX6E5IceybwAKcGX1b/83SXS+kkzb2qPynUA==", - "license": "Apache-2.0", - "dependencies": { - "@rollup/plugin-node-resolve": "15.2.3", - "@rollup/plugin-replace": "5.0.7", - "@rollup/plugin-typescript": "11.1.6", - "@rollup/plugin-wasm": "6.2.2", - "@vitejs/plugin-react": "4.3.4", - "esbuild": "0.25.8", - "fast-glob": "3.3.2", - "html-webpack-plugin": "5.6.0", - "mini-css-extract-plugin": "2.9.1", - "node-polyfill-webpack-plugin": "4.0.0", - "rollup": "4.12.0", - "rollup-plugin-import-css": "3.5.1", - "rollup-plugin-visualizer": "5.12.0", - "terser-webpack-plugin": "5.3.10", - "vite-plugin-no-bundle": "4.0.0", - "vite-tsconfig-paths": "5.1.4" - }, - "engines": { - "node": "20", - "npm": "10.8.2" - } - }, "packages/mongoose/node_modules/@prosopo/config/node_modules/@rollup/plugin-node-resolve": { "version": "15.2.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", @@ -34470,7 +34452,7 @@ "version": "22.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" diff --git a/packages/mongoose/package.json b/packages/mongoose/package.json index 745b46ccbf..1ba85e861a 100644 --- a/packages/mongoose/package.json +++ b/packages/mongoose/package.json @@ -27,13 +27,13 @@ }, "types": "dist/index.d.ts", "dependencies": { - "@prosopo/common": "3.1.21", + "@prosopo/common": "3.1.23", "mongodb": "6.15.0", "mongoose": "8.13.0", "zod": "3.23.8" }, "devDependencies": { - "@prosopo/config": "3.1.21", + "@prosopo/config": "3.1.23", "@types/node": "22.10.2", "@vitest/coverage-v8": "3.0.9", "concurrently": "9.0.1", From 9811d2f35fe131182edd2ab92548754503ba1028 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:36:15 +0000 Subject: [PATCH 12/12] Fix __v conflict in middleware and add changeset Co-authored-by: HughParry <90424587+HughParry@users.noreply.github.com> --- .changeset/mongoose-package-creation.md | 20 + package-lock.json | 546 +----------------------- packages/mongoose/src/middleware.ts | 6 +- packages/mongoose/src/zodMapper.ts | 3 +- 4 files changed, 39 insertions(+), 536 deletions(-) create mode 100644 .changeset/mongoose-package-creation.md diff --git a/.changeset/mongoose-package-creation.md b/.changeset/mongoose-package-creation.md new file mode 100644 index 0000000000..d4374eca6b --- /dev/null +++ b/.changeset/mongoose-package-creation.md @@ -0,0 +1,20 @@ +--- +"@prosopo/mongoose": minor +"@prosopo/database": minor +"@prosopo/types-database": minor +--- + +Create new @prosopo/mongoose package with standard middleware plugin and utilities. This package consolidates mongoose-related utilities and implements standard middleware as a Mongoose plugin for consistent data handling across the application. + +Key features: +- Centralized mongoose connection management with optimized timeout and heartbeat settings +- Standard middleware plugin for automatic timestamp management (createdAt, updatedAt) +- Version increment middleware for mutation operations +- Safe model creation using mongoose's overwriteModels flag +- Zod integration with enhanced validation +- All schemas created with helper functions have timestamps enabled by default + +Changes: +- New @prosopo/mongoose package with connection, middleware, schema, and zodMapper utilities +- Updated @prosopo/database to use createMongooseConnection() and getOrCreateModel() +- Updated @prosopo/types-database schemas to use newSchema() with automatic middleware application diff --git a/package-lock.json b/package-lock.json index 3a7f0f91af..79bd0dd3fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13809,18 +13809,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -15076,18 +15064,6 @@ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", "license": "MIT" }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -18620,15 +18596,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -18979,6 +18946,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, "license": "MIT" }, "node_modules/fast-uri": { @@ -19856,6 +19824,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, "license": "MIT" }, "node_modules/gopd": { @@ -20940,21 +20909,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "license": "MIT", - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -30500,6 +30454,7 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, "license": "MIT", "bin": { "tsconfck": "bin/tsconfck.js" @@ -31462,6 +31417,7 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -34093,6 +34049,7 @@ "version": "15.2.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "extraneous": true, "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -34118,6 +34075,7 @@ "version": "5.0.7", "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz", "integrity": "sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==", + "extraneous": true, "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -34139,6 +34097,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "extraneous": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -34151,6 +34110,7 @@ "version": "4.12.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", + "extraneous": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.5" @@ -34183,6 +34143,7 @@ "version": "3.5.1", "resolved": "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.5.1.tgz", "integrity": "sha512-cXgMPCUoDu64A2OFme4Is3eHmLiA54qTzxfvCbsORzro3C1adSe1fMMKUqfOUKTXROAPpW9PNDjpaGgPloGJOQ==", + "extraneous": true, "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.4" @@ -34198,6 +34159,7 @@ "version": "5.12.0", "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", + "extraneous": true, "license": "MIT", "dependencies": { "open": "^8.4.0", @@ -34220,232 +34182,11 @@ } } }, - "packages/mongoose/node_modules/@prosopo/locale": { - "version": "3.1.21", - "resolved": "https://registry.npmjs.org/@prosopo/locale/-/locale-3.1.21.tgz", - "integrity": "sha512-vqcUthgCyQg0D6gxzw6nfOp+F54UC+qUwK22JdxuoA4Pjvud6J44dZrbhbiJ8GK5dRZDMHTQjmeHpNWdxKbOxg==", - "license": "Apache-2.0", - "dependencies": { - "@prosopo/config": "3.1.21", - "i18next": "24.2.2", - "i18next-browser-languagedetector": "8.0.2", - "i18next-chained-backend": "4.6.2", - "i18next-fs-backend": "2.6.0", - "i18next-http-backend": "3.0.2", - "i18next-http-middleware": "3.7.4", - "i18next-resources-to-backend": "1.2.1", - "react-i18next": "15.4.0", - "zod": "3.23.8" - }, - "engines": { - "node": ">=v20.0.0", - "npm": ">=10.6.0" - } - }, - "packages/mongoose/node_modules/@prosopo/locale/node_modules/i18next": { - "version": "24.2.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.2.tgz", - "integrity": "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2" - }, - "peerDependencies": { - "typescript": "^5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "packages/mongoose/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", - "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-android-arm64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", - "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", - "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", - "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", - "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", - "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", - "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", - "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", - "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", - "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", - "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", - "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "packages/mongoose/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", - "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "packages/mongoose/node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, "license": "MIT" }, "packages/mongoose/node_modules/@types/node": { @@ -34458,25 +34199,6 @@ "undici-types": "~6.20.0" } }, - "packages/mongoose/node_modules/@vitejs/plugin-react": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", - "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" - } - }, "packages/mongoose/node_modules/@vitest/coverage-v8": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.9.tgz", @@ -34596,55 +34318,6 @@ "url": "https://opencollective.com/vitest" } }, - "packages/mongoose/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "packages/mongoose/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "packages/mongoose/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "packages/mongoose/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -34663,31 +34336,11 @@ } } }, - "packages/mongoose/node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "packages/mongoose/node_modules/domain-browser": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-5.7.0.tgz", - "integrity": "sha512-edTFu0M/7wO1pXY6GDxVNVW086uqwWYIHP98txhcPyV995X21JIH2DtYp33sQJOupYoXKe9RwTw2Ya2vWaquTQ==", - "license": "Artistic-2.0", - "engines": { - "node": ">=4" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, "packages/mongoose/node_modules/esbuild": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -34767,39 +34420,6 @@ "node": ">=8" } }, - "packages/mongoose/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/mongoose/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "packages/mongoose/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, "packages/mongoose/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -34852,61 +34472,6 @@ "node": ">=16.20.1" } }, - "packages/mongoose/node_modules/node-polyfill-webpack-plugin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-WLk77vLpbcpmTekRj6s6vYxk30XoyaY5MDZ4+9g8OaKoG3Ij+TjOqhpQjVUlfDZBPBgpNATDltaQkzuXSnnkwg==", - "license": "MIT", - "dependencies": { - "assert": "^2.1.0", - "browserify-zlib": "^0.2.0", - "buffer": "^6.0.3", - "console-browserify": "^1.2.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.12.0", - "domain-browser": "^5.7.0", - "events": "^3.3.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "^1.0.1", - "process": "^0.11.10", - "punycode": "^2.3.1", - "querystring-es3": "^0.2.1", - "readable-stream": "^4.5.2", - "stream-browserify": "^3.0.0", - "stream-http": "^3.2.0", - "string_decoder": "^1.3.0", - "timers-browserify": "^2.0.12", - "tty-browserify": "^0.0.1", - "type-fest": "^4.18.2", - "url": "^0.11.3", - "util": "^0.12.5", - "vm-browserify": "^1.1.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "webpack": ">=5" - } - }, - "packages/mongoose/node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/mongoose/node_modules/p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -34933,92 +34498,6 @@ "node": ">=8" } }, - "packages/mongoose/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" - } - }, - "packages/mongoose/node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "packages/mongoose/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "packages/mongoose/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "packages/mongoose/node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, "packages/mongoose/node_modules/tinyspy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", @@ -35033,6 +34512,7 @@ "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", diff --git a/packages/mongoose/src/middleware.ts b/packages/mongoose/src/middleware.ts index 31afaefc69..114e0de785 100644 --- a/packages/mongoose/src/middleware.ts +++ b/packages/mongoose/src/middleware.ts @@ -67,20 +67,22 @@ export function standardMiddlewarePlugin(schema: Schema, options?: unknown schema.pre(method as any, function (this: Query, next: (err?: Error) => void) { const update = this.getUpdate(); - // Increment __v + // Increment __v and manage timestamps if (update && typeof update === "object") { // Handle both $set and direct updates if ("$set" in update) { (update.$set as Record).updatedAt = new Date(); // Prevent createdAt from being overwritten delete (update.$set as Record).createdAt; + // Remove __v from $set if present to avoid conflict with $inc + delete (update.$set as Record).__v; } else if (!("$setOnInsert" in update)) { (update as Record).updatedAt = new Date(); // Prevent createdAt from being overwritten delete (update as Record).createdAt; } - // Increment version + // Increment version - only if not using $set with __v if ("$inc" in update) { (update.$inc as Record).__v = ((update.$inc as Record).__v || 0) + 1; diff --git a/packages/mongoose/src/zodMapper.ts b/packages/mongoose/src/zodMapper.ts index ea2a9b8b78..72230e4a93 100644 --- a/packages/mongoose/src/zodMapper.ts +++ b/packages/mongoose/src/zodMapper.ts @@ -178,8 +178,9 @@ export function createModelFromZodSchema( ] as const; for (const method of updateMethods) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any schema.pre( - method, + method as any, async function ( this: Query, next: (err?: Error) => void,