@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 +21 -0
- package/README.md +339 -0
- package/dist/engine.d.ts +28 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +190 -0
- package/dist/odm.d.ts +21 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +10 -0
- package/dist/transforms.d.ts +16 -0
- package/package.json +51 -0
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
|
package/dist/engine.d.ts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|
package/dist/schema.d.ts
ADDED
|
@@ -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
|
+
}
|