@mauroandre/zodmongo 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mauro André
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,339 @@
1
+ # ZodMongo
2
+
3
+ Lightweight MongoDB ODM powered by [Zod](https://zod.dev) schemas. TypeScript-first, built on the native MongoDB driver — no Mongoose.
4
+
5
+ ## Why ZodMongo?
6
+
7
+ - **No Mongoose** — uses the native MongoDB driver, zero overhead
8
+ - **Zod-native** — define schemas with Zod, not a proprietary format
9
+ - **TypeScript-first** — types inferred directly from your Zod schemas
10
+ - **Tiny** — ~300 lines of code, only `mongodb` and `zod` as dependencies
11
+ - **Transparent id/ObjectId** — work with `id` (string) in your app, `_id` (ObjectId) in MongoDB
12
+ - **Automatic timestamps** — `createdAt` and `updatedAt` managed by the ODM
13
+ - **Built-in pagination** — via aggregation pipeline with `$facet`
14
+ - **Full aggregation pipeline** — `findMany` accepts a complete pipeline, not just match filters
15
+ - **Relations & Snapshots** — declare references to other collections with automatic `$lookup` generation
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install zodmongo
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ import { connect, close, save, findMany, deleteMany } from "zodmongo";
27
+ import { dbSchema } from "zodmongo";
28
+ import { z } from "zod/v4";
29
+
30
+ // Connect
31
+ await connect("mongodb://localhost:27017", "mydb");
32
+
33
+ // Define a schema
34
+ const userSchema = dbSchema({
35
+ name: z.string(),
36
+ email: z.string().email(),
37
+ });
38
+ type User = z.infer<typeof userSchema>;
39
+
40
+ // Insert
41
+ const user = userSchema.parse({ name: "Mauro", email: "mauro@example.com" });
42
+ await save("users", user);
43
+ console.log(user.id); // ObjectId string, auto-assigned
44
+
45
+ // Update
46
+ user.name = "Mauro André";
47
+ await save("users", user);
48
+
49
+ // Find
50
+ const users = await findMany<User>("users", { name: "Mauro André" });
51
+ const all = await findMany<User>("users");
52
+
53
+ // Delete
54
+ await deleteMany("users", { email: "mauro@example.com" });
55
+
56
+ // Close
57
+ await close();
58
+ ```
59
+
60
+ ## Schemas
61
+
62
+ ### `dbSchema(shape)`
63
+
64
+ Creates a schema that extends the base model with `id`, `createdAt`, and `updatedAt`. This is the primary way to define your models.
65
+
66
+ ```typescript
67
+ import { dbSchema } from "zodmongo";
68
+ import { z } from "zod/v4";
69
+
70
+ const postSchema = dbSchema({
71
+ title: z.string(),
72
+ body: z.string(),
73
+ published: z.boolean().default(false),
74
+ });
75
+ type Post = z.infer<typeof postSchema>;
76
+ // { id: string | null, createdAt: Date | null, updatedAt: Date | null, title: string, body: string, published: boolean }
77
+ ```
78
+
79
+ ### `embeddedSchema(shape)`
80
+
81
+ Creates a schema **without** the base model fields (`id`, `createdAt`, `updatedAt`). Use for nested objects that don't need their own identity.
82
+
83
+ ```typescript
84
+ import { embeddedSchema } from "zodmongo";
85
+ import { z } from "zod/v4";
86
+
87
+ const addressSchema = embeddedSchema({
88
+ street: z.string(),
89
+ city: z.string(),
90
+ zip: z.string(),
91
+ });
92
+
93
+ const userSchema = dbSchema({
94
+ name: z.string(),
95
+ address: addressSchema,
96
+ });
97
+ ```
98
+
99
+ ### `dbModelSchema` and `idSchema`
100
+
101
+ Low-level schemas if you need to extend manually:
102
+
103
+ ```typescript
104
+ import { dbModelSchema, idSchema } from "zodmongo/schema";
105
+
106
+ const customSchema = dbModelSchema.extend({
107
+ name: z.string(),
108
+ });
109
+
110
+ // idSchema validates a string as a valid ObjectId
111
+ idSchema.parse("507f1f77bcf86cd799439011"); // ok
112
+ idSchema.parse("invalid"); // throws
113
+ ```
114
+
115
+ ## API
116
+
117
+ ### `connect(uri, dbName)`
118
+
119
+ Connects to MongoDB. Returns the `Db` instance.
120
+
121
+ ```typescript
122
+ const db = await connect("mongodb://localhost:27017", "mydb");
123
+ ```
124
+
125
+ ### `close()`
126
+
127
+ Waits for pending promises and closes the connection.
128
+
129
+ ```typescript
130
+ await close();
131
+ ```
132
+
133
+ ### `getDb()`
134
+
135
+ Returns the current `Db` instance. Throws if not connected.
136
+
137
+ ```typescript
138
+ const db = getDb();
139
+ const collection = db.collection("users");
140
+ ```
141
+
142
+ ### `save(collection, doc, filter?, options?)`
143
+
144
+ Smart upsert — inserts if no `id`, updates if `id` exists. Automatically manages `createdAt` and `updatedAt`.
145
+
146
+ ```typescript
147
+ // Insert (no id)
148
+ const user = userSchema.parse({ name: "Mauro", email: "m@b.com" });
149
+ await save("users", user);
150
+ // user.id is now set to the generated ObjectId string
151
+
152
+ // Update (has id)
153
+ user.name = "Updated";
154
+ await save("users", user);
155
+
156
+ // Upsert with custom filter
157
+ await save("users", user, { email: "m@b.com" });
158
+
159
+ // Disable upsert (update only, no insert)
160
+ await save("users", user, { email: "m@b.com" }, { upsert: false });
161
+ ```
162
+
163
+ ### `findMany<T>(collection, matchOrPipeline?, options?)`
164
+
165
+ Finds documents using a simple match object or a full aggregation pipeline. Automatically converts `_id` to `id` and ObjectIds to strings in the results.
166
+
167
+ ```typescript
168
+ // All documents
169
+ const all = await findMany<User>("users");
170
+
171
+ // Simple match
172
+ const admins = await findMany<User>("users", { role: "admin" });
173
+
174
+ // Find by id
175
+ const found = await findMany<User>("users", { id: "507f1f77bcf86cd799439011" });
176
+
177
+ // Full aggregation pipeline
178
+ const topAdmins = await findMany<User>("users", [
179
+ { $match: { role: "admin" } },
180
+ { $sort: { createdAt: -1 } },
181
+ { $limit: 10 },
182
+ ]);
183
+ ```
184
+
185
+ #### Pagination
186
+
187
+ Pass `{ paginate: true }` to get paginated results via `$facet`:
188
+
189
+ ```typescript
190
+ const page = await findMany<User>("users", {}, {
191
+ paginate: true,
192
+ currentPage: 1,
193
+ docsPerPage: 20,
194
+ });
195
+
196
+ page.docs; // User[]
197
+ page.currentPage; // 1
198
+ page.pageQuantity; // total pages
199
+ page.docsQuantity; // total documents
200
+ ```
201
+
202
+ ### `deleteMany(collection, filter)`
203
+
204
+ Deletes documents matching the filter. Automatically converts `id` to `_id`.
205
+
206
+ ```typescript
207
+ await deleteMany("users", { email: "m@b.com" });
208
+ await deleteMany("users", { id: "507f1f77bcf86cd799439011" });
209
+ ```
210
+
211
+ ## Relations
212
+
213
+ Declare references between collections. The ODM generates `$lookup` pipelines automatically.
214
+
215
+ ### `relation(schema, config)`
216
+
217
+ Marks a field as a reference to another collection.
218
+
219
+ ```typescript
220
+ const companySchema = dbSchema({ name: z.string() });
221
+
222
+ const userSchema = dbSchema({
223
+ name: z.string(),
224
+ company: relation(companySchema, { collection: "companies" }),
225
+ });
226
+
227
+ // When fetching, use getPipeline() to auto-generate $lookup stages
228
+ const pipeline = getPipeline(userSchema);
229
+ const users = await findMany<User>("users", pipeline);
230
+ // users[0].company is now the full company document, not just an ObjectId
231
+ ```
232
+
233
+ When saving, use `toSave()` to convert relations back to ObjectIds:
234
+
235
+ ```typescript
236
+ const dataToSave = toSave(userSchema, userData);
237
+ // dataToSave.company is now an ObjectId
238
+ await save("users", dataToSave);
239
+ ```
240
+
241
+ #### Array relations
242
+
243
+ ```typescript
244
+ const tagSchema = dbSchema({ label: z.string() });
245
+
246
+ const postSchema = dbSchema({
247
+ title: z.string(),
248
+ tags: z.array(relation(tagSchema, { collection: "tags" })),
249
+ });
250
+ ```
251
+
252
+ #### Custom foreign field
253
+
254
+ ```typescript
255
+ const categorySchema = dbSchema({ slug: z.string(), name: z.string() });
256
+
257
+ const postSchema = dbSchema({
258
+ title: z.string(),
259
+ category: relation(categorySchema, { collection: "categories", foreignField: "slug" }),
260
+ });
261
+ ```
262
+
263
+ ### `snapshot(schema)`
264
+
265
+ Marks a field as a persisted copy. The ODM will **not** generate `$lookup` for it and will **not** convert it to ObjectId when saving. Useful for denormalized data you want to store as-is.
266
+
267
+ ```typescript
268
+ const userSchema = dbSchema({
269
+ name: z.string(),
270
+ company: snapshot(companySchema), // stored as a full copy, no lookup
271
+ });
272
+ ```
273
+
274
+ ### `getPipeline(schema)`
275
+
276
+ Generates the aggregation pipeline (with `$lookup`, `$set`, `$project`) from a schema.
277
+
278
+ ```typescript
279
+ const pipeline = getPipeline(userSchema);
280
+ // Use it with findMany
281
+ const users = await findMany<User>("users", [...pipeline, { $match: { active: true } }]);
282
+ ```
283
+
284
+ ### `toSave(schema, data)`
285
+
286
+ Parses data through the schema and converts relations to ObjectIds for saving.
287
+
288
+ ```typescript
289
+ const prepared = toSave(userSchema, rawData);
290
+ await save("users", prepared);
291
+ ```
292
+
293
+ ## id ↔ _id ↔ ObjectId
294
+
295
+ ZodMongo automatically handles conversions between your app's `id` (string) and MongoDB's `_id` (ObjectId):
296
+
297
+ | Direction | What happens |
298
+ |---|---|
299
+ | **Reading** (MongoDB → App) | `_id` (ObjectId) becomes `id` (string), recursively |
300
+ | **Saving** (App → MongoDB) | `id` (string) becomes `_id` (ObjectId), valid ObjectId strings are converted |
301
+ | **Querying** | `{ id: "..." }` becomes `{ _id: ObjectId("...") }`, supports dot-notation (`company.id` → `company._id`) |
302
+
303
+ ## Automatic Timestamps
304
+
305
+ - `createdAt` — set automatically on insert (`$setOnInsert`), never modified on update
306
+ - `updatedAt` — set on every save (`$set`)
307
+
308
+ ## Promise Tracking
309
+
310
+ Use `trackPromise()` to register fire-and-forget operations. `close()` waits for all tracked promises before disconnecting.
311
+
312
+ ```typescript
313
+ import { trackPromise, close } from "zodmongo";
314
+
315
+ trackPromise(save("logs", logEntry));
316
+ trackPromise(save("logs", anotherEntry));
317
+
318
+ await close(); // waits for both saves to complete
319
+ ```
320
+
321
+ ## Development
322
+
323
+ ```bash
324
+ # Start MongoDB
325
+ docker compose up -d
326
+
327
+ # Run tests
328
+ npm test
329
+
330
+ # Type check
331
+ npm run typecheck
332
+
333
+ # Build
334
+ npm run build
335
+ ```
336
+
337
+ ## License
338
+
339
+ MIT
@@ -0,0 +1,28 @@
1
+ import { Db, Document, UpdateResult } from 'mongodb';
2
+ import { DbModel } from './schema.js';
3
+ declare const trackPromise: <T>(promise: Promise<T>) => Promise<T>;
4
+ declare const connect: (uri: string, dbName: string) => Promise<Db>;
5
+ declare const getDb: () => Db;
6
+ declare const close: () => Promise<void>;
7
+ interface PaginateResponse<T> {
8
+ currentPage: number;
9
+ pageQuantity: number;
10
+ docsQuantity: number;
11
+ docs: T[];
12
+ }
13
+ interface FindOptions {
14
+ paginate?: boolean;
15
+ currentPage?: number;
16
+ docsPerPage?: number;
17
+ }
18
+ interface SaveOptions {
19
+ upsert?: boolean;
20
+ }
21
+ declare const save: <T extends DbModel>(collectionName: string, doc: T, find?: any, options?: SaveOptions) => Promise<UpdateResult>;
22
+ declare function findMany<T>(collectionName: string, matchOrPipeline: Document[] | any, options: FindOptions & {
23
+ paginate: true;
24
+ }): Promise<PaginateResponse<T>>;
25
+ declare function findMany<T>(collectionName: string, matchOrPipeline?: Document[] | any, options?: FindOptions): Promise<T[]>;
26
+ declare const deleteMany: (collectionName: string, filter: any) => Promise<import('mongodb').DeleteResult>;
27
+ export { connect, getDb, close, save, findMany, deleteMany, trackPromise };
28
+ export type { PaginateResponse, FindOptions, SaveOptions };
@@ -0,0 +1,6 @@
1
+ export { connect, getDb, close, save, findMany, deleteMany, trackPromise, } from './engine.js';
2
+ export { dbModelSchema, idSchema } from './schema.js';
3
+ export { dbSchema, embeddedSchema, relation, snapshot, getPipeline, toSave, } from './odm.js';
4
+ export type { PaginateResponse, FindOptions, SaveOptions } from './engine.js';
5
+ export type { DbModel, Id } from './schema.js';
6
+ export type { RelationConfig, SchemaWithPipeline } from './odm.js';
package/dist/index.js ADDED
@@ -0,0 +1,190 @@
1
+ import { dbModelSchema as e, idSchema as t } from "./schema.js";
2
+ import { MongoClient as n, ObjectId as r } from "mongodb";
3
+ import { z as i } from "zod/v4";
4
+ //#region src/transforms.ts
5
+ var a = (e) => {
6
+ if (!e || typeof e != "object") return e;
7
+ if (e instanceof r) return e.toString();
8
+ if (Array.isArray(e)) return e.map(a);
9
+ e._id && (e._id instanceof r ? e.id = e._id.toString() : e.id = e._id, delete e._id);
10
+ for (let t in e) e[t] && (e[t] instanceof r ? e[t] = e[t].toString() : typeof e[t] == "object" && (e[t] = a(e[t])));
11
+ return e;
12
+ }, o = (e) => {
13
+ if (typeof e == "string" && r.isValid(e)) return new r(e);
14
+ if (!e || typeof e != "object" || e instanceof r) return e;
15
+ if (Array.isArray(e)) return e.map(o);
16
+ e.id && typeof e.id == "string" && (e._id = new r(e.id), delete e.id);
17
+ for (let t in e) if (e[t]) {
18
+ if (e[t] instanceof r) continue;
19
+ (typeof e[t] == "string" || typeof e[t] == "object") && (e[t] = o(e[t]));
20
+ }
21
+ return e;
22
+ }, s = (e) => {
23
+ if (!e || typeof e != "object") return typeof e == "string" && r.isValid(e) ? new r(e) : e;
24
+ if (e instanceof r || e instanceof Date) return e;
25
+ if (Array.isArray(e)) return e.map(s);
26
+ let t = {};
27
+ for (let n in e) {
28
+ let r = n === "id" ? "_id" : n.endsWith(".id") ? n.slice(0, -3) + "._id" : n;
29
+ t[r] = s(e[n]);
30
+ }
31
+ return t;
32
+ }, c, l, u = /* @__PURE__ */ new Set(), d = (e) => (u.add(e), e.finally(() => u.delete(e)), e), f = async () => {
33
+ u.size > 0 && await Promise.all(u);
34
+ }, p = async (e, t) => c || (l = new n(e), await l.connect(), c = l.db(t), c), m = () => {
35
+ if (!c) throw Error("Database not initialized. Call connect() first.");
36
+ return c;
37
+ }, h = async () => {
38
+ l &&= (await f(), await l.close(), c = void 0, void 0);
39
+ }, g = async (e, t, n, i = { upsert: !0 }) => {
40
+ let a = m(), s = /* @__PURE__ */ new Date(), { id: c, createdAt: l, updatedAt: u, ...d } = t, f = o({ ...d }), p = n || (c ? { _id: new r(c) } : { _id: new r() });
41
+ p.id && (p = {
42
+ ...p,
43
+ _id: new r(p.id)
44
+ }, delete p.id), p._id && typeof p._id == "string" && (p = {
45
+ ...p,
46
+ _id: new r(p._id)
47
+ });
48
+ let h = await a.collection(e).updateMany(p, {
49
+ $set: {
50
+ ...f,
51
+ updatedAt: s
52
+ },
53
+ $setOnInsert: { createdAt: s }
54
+ }, { upsert: i.upsert ?? !0 });
55
+ return h.upsertedId && (t.id = h.upsertedId.toString()), h;
56
+ };
57
+ async function _(e, t, n = {}) {
58
+ let r = m().collection(e), i;
59
+ i = Array.isArray(t) ? t : t ? [{ $match: t }] : [];
60
+ for (let e of i) e.$match &&= s(e.$match);
61
+ if (n.paginate) {
62
+ let e = n.currentPage && n.currentPage > 0 ? n.currentPage : 1, t = n.docsPerPage && n.docsPerPage > 0 ? n.docsPerPage : 100, o = (e - 1) * t, s = [...i, { $facet: {
63
+ docs: [{ $skip: o }, { $limit: t }],
64
+ docsQuantity: [{ $count: "count" }]
65
+ } }], c = await r.aggregate(s).toArray(), l = (c[0]?.docs || []).map(a), u = c[0]?.docsQuantity[0]?.count || 0;
66
+ return {
67
+ docs: l,
68
+ docsQuantity: u,
69
+ pageQuantity: Math.ceil(u / t),
70
+ currentPage: e
71
+ };
72
+ }
73
+ return (await r.aggregate(i).toArray()).map(a);
74
+ }
75
+ var v = async (e, t) => {
76
+ let n = s(t);
77
+ return await m().collection(e).deleteMany(n);
78
+ }, y = (e) => e._zod?.def ?? e._def ?? {}, b = (e) => {
79
+ let t = y(e);
80
+ return t.type ?? t.typeName ?? "";
81
+ }, x = (e) => {
82
+ let t = e, n = b(t);
83
+ for (; n === "optional" || n === "nullable" || n === "default" || n === "pipe";) {
84
+ let e = y(t);
85
+ t = n === "pipe" ? e.in ?? t : e.innerType ?? t, n = b(t);
86
+ }
87
+ return t;
88
+ }, S = (e) => e._relation, C = (e) => e._snapshot === !0, w = (e) => typeof e.pipeline == "function", T = (e) => b(e) === "object", E = (e) => b(e) === "array", D = (e) => y(e).element, O = (e) => y(e).shape, k = (e) => {
89
+ let t = [], n = {}, r = (e, i) => {
90
+ let a = x(e);
91
+ if (E(a)) {
92
+ let e = D(a);
93
+ if (!e) return;
94
+ let o = x(e);
95
+ if (C(e) || C(o)) {
96
+ n[i] = 1;
97
+ return;
98
+ }
99
+ let s = S(e) ?? S(o);
100
+ if (s) {
101
+ let r = w(e) ? e.pipeline() : w(o) ? o.pipeline() : [];
102
+ t.push({ $lookup: {
103
+ from: s.collection,
104
+ localField: i,
105
+ foreignField: s.foreignField ?? "_id",
106
+ as: i,
107
+ ...r.length > 0 && { pipeline: r }
108
+ } }), n[i] = 1;
109
+ } else T(o) ? r(e, i) : n[i] = 1;
110
+ return;
111
+ }
112
+ if (T(a)) {
113
+ let e = O(a);
114
+ if (!e) return;
115
+ for (let [a, o] of Object.entries(e)) {
116
+ let e = a === "id" ? "_id" : a, s = i ? `${i}.${e}` : e, c = x(o);
117
+ if (C(o) || C(c)) {
118
+ n[s] = 1;
119
+ continue;
120
+ }
121
+ let l = S(o) ?? S(c);
122
+ if (l) {
123
+ let e = w(o) ? o.pipeline() : w(c) ? c.pipeline() : [];
124
+ t.push({ $lookup: {
125
+ from: l.collection,
126
+ localField: s,
127
+ foreignField: l.foreignField ?? "_id",
128
+ as: s,
129
+ ...e.length > 0 && { pipeline: e }
130
+ } }), t.push({ $set: { [s]: { $arrayElemAt: [`$${s}`, 0] } } }), n[s] = 1;
131
+ } else T(c) || E(c) ? r(o, s) : n[s] = 1;
132
+ }
133
+ }
134
+ };
135
+ return r(e, ""), Object.keys(n).length > 0 && t.push({ $project: n }), t;
136
+ }, A = (e, t) => {
137
+ if (t == null) return t;
138
+ let n = x(e);
139
+ if (C(e) || C(n)) return t;
140
+ if (S(e) ?? S(n)) return new r(t.id);
141
+ if (E(n) && Array.isArray(t)) {
142
+ let e = D(n);
143
+ return e ? t.map((t) => A(e, t)) : t;
144
+ }
145
+ if (T(n) && typeof t == "object") {
146
+ let e = O(n);
147
+ if (!e) return t;
148
+ let r = { ...t };
149
+ for (let [n, i] of Object.entries(e)) n in t && (r[n] = A(i, t[n]));
150
+ return r;
151
+ }
152
+ return t;
153
+ }, j = (e) => {
154
+ if (w(e)) return e.pipeline();
155
+ let t = x(e);
156
+ return w(t) ? t.pipeline() : k(t);
157
+ }, M = (e) => typeof e.toSave == "function", N = (e, t) => {
158
+ if (M(e)) return e.toSave(t);
159
+ let n = e.parse(t);
160
+ return A(x(e), n);
161
+ }, P = (e, t) => Object.assign(e, { _relation: t }), F = (e) => {
162
+ let t = Object.create(e);
163
+ return t._snapshot = !0, t._relation = void 0, t;
164
+ }, I = (t) => {
165
+ let n = e.extend(t), r = () => k(n), i = (e) => A(n, n.parse(e)), a = n.transform.bind(n);
166
+ return n.transform = (e) => {
167
+ let t = a(e);
168
+ return Object.assign(t, {
169
+ pipeline: r,
170
+ toSave: (e) => A(n, t.parse(e))
171
+ });
172
+ }, Object.assign(n, {
173
+ pipeline: r,
174
+ toSave: i
175
+ });
176
+ }, L = (e) => {
177
+ let t = i.object(e), n = () => k(t), r = (e) => A(t, t.parse(e)), a = t.transform.bind(t);
178
+ return t.transform = (e) => {
179
+ let r = a(e);
180
+ return Object.assign(r, {
181
+ pipeline: n,
182
+ toSave: (e) => A(t, r.parse(e))
183
+ });
184
+ }, Object.assign(t, {
185
+ pipeline: n,
186
+ toSave: r
187
+ });
188
+ };
189
+ //#endregion
190
+ export { h as close, p as connect, e as dbModelSchema, I as dbSchema, v as deleteMany, L as embeddedSchema, _ as findMany, m as getDb, j as getPipeline, t as idSchema, P as relation, g as save, F as snapshot, N as toSave, d as trackPromise };
package/dist/odm.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod/v4';
2
+ import { Document } from 'mongodb';
3
+ import { dbModelSchema } from './schema.js';
4
+ interface RelationConfig {
5
+ collection: string;
6
+ foreignField?: string;
7
+ }
8
+ interface RelationMeta {
9
+ _relation?: RelationConfig;
10
+ }
11
+ type SchemaWithPipeline<T extends z.ZodTypeAny> = T & {
12
+ pipeline: () => Document[];
13
+ toSave: (data: z.input<T>) => z.output<T>;
14
+ };
15
+ export declare const getPipeline: (schema: z.ZodTypeAny) => Document[];
16
+ export declare const toSave: <T extends z.ZodTypeAny>(schema: T, data: z.input<T>) => z.output<T>;
17
+ export declare const relation: <T extends z.ZodTypeAny>(schema: T, config: RelationConfig) => T & RelationMeta;
18
+ export declare const snapshot: <T extends z.ZodTypeAny>(schema: T) => T;
19
+ export declare const dbSchema: <T extends z.ZodRawShape>(shape: T) => SchemaWithPipeline<ReturnType<typeof dbModelSchema.extend<T>>>;
20
+ export declare const embeddedSchema: <T extends z.ZodRawShape>(shape: T) => SchemaWithPipeline<z.ZodObject<T>>;
21
+ export type { RelationConfig, SchemaWithPipeline };
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod/v4';
2
+ export declare const idSchema: z.ZodString;
3
+ export declare const dbModelSchema: z.ZodObject<{
4
+ id: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
5
+ createdAt: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
6
+ updatedAt: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
7
+ }, z.core.$strip>;
8
+ export type DbModel = z.infer<typeof dbModelSchema>;
9
+ export type Id = z.infer<typeof idSchema>;
package/dist/schema.js ADDED
@@ -0,0 +1,10 @@
1
+ import { ObjectId as e } from "mongodb";
2
+ import { z as t } from "zod/v4";
3
+ //#region src/schema.ts
4
+ var n = t.string().refine((t) => e.isValid(t), { message: "Invalid ObjectId format" }), r = t.object({
5
+ id: n.nullable().optional().default(null),
6
+ createdAt: t.coerce.date().nullable().optional().default(null),
7
+ updatedAt: t.coerce.date().nullable().optional().default(null)
8
+ });
9
+ //#endregion
10
+ export { r as dbModelSchema, n as idSchema };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Recursively converts _id (ObjectId) to id (string).
3
+ * Used to transform documents coming from MongoDB into the application format.
4
+ */
5
+ export declare const transformDoc: <T>(doc: any) => T;
6
+ /**
7
+ * Recursively converts id (string) to _id (ObjectId).
8
+ * Automatically converts valid ObjectId strings.
9
+ * Used before saving documents to MongoDB.
10
+ */
11
+ export declare const transformDocForSave: (doc: any) => any;
12
+ /**
13
+ * Converts valid strings to ObjectIds and id to _id in search queries.
14
+ * Supports dot-notation (e.g. "company.id" → "company._id").
15
+ */
16
+ export declare const transformMatchQuery: (query: any) => any;
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@mauroandre/zodmongo",
3
+ "version": "0.0.1",
4
+ "description": "Lightweight MongoDB ODM with Zod validation. TypeScript-first, no Mongoose.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Mauro André",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/mauro-andre/zodmongo.git"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ },
17
+ "./schema": {
18
+ "import": "./dist/schema.js",
19
+ "types": "./dist/schema.d.ts"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "vite build",
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "vitest run --reporter verbose",
29
+ "test:watch": "vitest --reporter verbose",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "keywords": [
33
+ "mongodb",
34
+ "zod",
35
+ "odm",
36
+ "typescript",
37
+ "validation",
38
+ "schema"
39
+ ],
40
+ "dependencies": {
41
+ "mongodb": "^7.1.1",
42
+ "zod": "^4.3.6"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^25.6.0",
46
+ "typescript": "^6.0.2",
47
+ "vite": "^8.0.8",
48
+ "vite-plugin-dts": "^4.5.4",
49
+ "vitest": "^4.1.4"
50
+ }
51
+ }