Upgrade from 0.2.x to 0.3.x
Summary: Mondel 0.3 keeps the public API surface used in 0.2 (method names, argument order, createClient overloads, validation modes, schema builders).
It is a minor release: additive features plus intentional behavior fixes. It is not a bit-for-bit behavioral freeze of every 0.2 edge case.
| Claim | Accurate? |
|---|---|
| Stable public API (no removed methods / renames) | Yes |
| Safe for most apps that write valid data | Yes |
| Zero behavior change in every scenario | No |
| “100% compatible bit-for-bit with 0.2” | No — do not promise that |
Peers unchanged: mongodb ^6 \|\| ^7, zod ^3.24 \|\| ^4, Node >=18.
What stays the same
defineSchema/schema/s.*chainscreateClientserverless factory vs nodeurimode- Collection methods:
find*,create*,update*,delete*,count,exists,aggregate,getCollection - Validation modes:
"strict"|"loose"|"off" - Hard delete when
softis omitted - Native MongoDB filters and non-
$setoperators ($inc,$push, …) - CLI
pull/pushcommands and config shape
Additive only (opt-in): findOneAndUpdate, bulkWrite, richer find option passthrough (timeoutMS, …).
Behavior differences (review before upgrading)
1. Field .default() on create
| 0.2.x | 0.3.x | |
|---|---|---|
create / createMany with validation strict or loose | Default often not written to the document | Default is applied (all nesting levels) |
validation: "off" | No defaults | No defaults (unchanged) |
| Updates | Should not inject defaults | Do not inject defaults (including nested) |
Who cares: schemas that use .default(...) and omit the field on create.
Action: confirm stored documents should include those defaults (usually yes — matches schema docs).
2. $set / $setOnInsert validation (strict)
| 0.2.x | 0.3.x | |
|---|---|---|
Plain update { name: "x" } | Validated | Validated |
{ $set: { age: -1 } } with min constraint, strict | Often accepted | ZodError |
{ $inc: { age: 1 } } | Not validated | Not validated |
Who cares: code that pushed invalid shapes only via $set under validation: "strict".
Action: fix payloads, or temporarily use "loose" / "off" during migration.
3. timestamps: false on updates
| 0.2.x | 0.3.x | |
|---|---|---|
updateOne(..., { timestamps: false }) | Could still set updatedAt (bug) | Does not set timestamp fields |
Who cares: callers that passed timestamps: false and relied on timestamps still appearing.
Action: omit the flag if you want automatic updatedAt.
4. { soft: true } on delete
| 0.2.x | 0.3.x | |
|---|---|---|
deleteOne(filter) | Hard delete | Hard delete (unchanged) |
deleteOne(filter, { soft: true }) | Flag ignored → hard delete | Sets deletedAt (+ updatedAt if timestamps enabled) |
Who cares: only apps that already passed soft: true.
Action: if you meant hard delete, remove { soft: true }. If you want soft delete, filter reads with deletedAt: { $exists: false }.
Soft-delete deletedCount reflects matched documents.
5. TypeScript type of create result
| 0.2.x types | 0.3.x types | Runtime (both) | |
|---|---|---|---|
create() | Incorrectly looked like full document + insertedId | InsertOneResult | Driver insert result (insertedId, …) |
Who cares: TypeScript code that read result.email (etc.) after create.
Action: use result.insertedId; fetch the doc with findById if you need fields. Runtime was never a full document.
6. createMany([])
| 0.2.x | 0.3.x | |
|---|---|---|
| Empty array | Could error in the driver | No-op, insertedCount: 0 |
Safer; unlikely to break real apps.
7. s.objectId() accepts only real ObjectId values
| 0.2.x | 0.3.x | |
|---|---|---|
s.objectId() string input | Any string passed | Must be a 24-character hex string |
0.2.x built this field as z.union([z.string(), …]), so the first branch accepted every string and the check was effectively dead. 0.3.x requires an ObjectId instance or a 24-char hex string.
Who cares: collections whose objectId fields hold legacy or synthetic ids ("", numeric strings, external keys).
Action: grep your schemas for s.objectId() and confirm the data really is ObjectId-shaped. If a field holds arbitrary identifiers, model it as s.string().
// legacy/external identifiers — not ObjectIds
brandRef: s.string(),Pre-upgrade checklist
- [ ]
npm install mondel@^0.3.1(keep existingmongodb/zodpeers) - [ ] Run
tsc— fix any code treatingcreateresult as a full document - [ ] Grep schemas for
.default(— confirm create paths that omit those fields - [ ] Grep for
$setundervalidation: "strict"— ensure values pass schema rules - [ ] Grep schemas for
s.objectId()— confirm the stored values are ObjectId-shaped - [ ] Grep for
timestamps: falseon updates — confirm intent - [ ] Grep for
soft: true— confirm soft-delete is desired - [ ] Map
ZodErrorto a 400 at your API boundary — strict mode now rejects payloads that 0.2.x silently accepted, and the defaultErrorhandler will report them as 500s - [ ] Compare enum fields against real data (
db.col.distinct("status")) — a value in the collection that is missing froms.enum([...])now fails$set - [ ] Smoke-test create / update / delete in a staging DB
# Optional: temporarily relax validation while fixing write paths
# validation: "loose" // warn instead of throw, defaults still appliedPrefer "loose" over "off" for that: off also disables field defaults, so it changes what gets written as well as what gets rejected.
Install
npm install mondel@^0.3.1
# peers unchanged
npm install mongodb@^6 zod@^3 # or mongodb@^7 + Node 20+, zod@^4Go straight to 0.3.1 — it fixes a payload-mutation bug and makes defaults deterministic in loose mode. See the CHANGELOG.
Related
- CHANGELOG
- Compatibility (MongoDB / Node / driver matrix)
- Validation
- Soft delete recipe
