Skip to content

Releases: copleykj/meteor-typed-model

v2.0.0 — Zod 4

Choose a tag to compare

@copleykj copleykj released this 23 Jul 00:25

⚠️ Breaking Changes

Zod 4 required. This release targets Zod 4 (^4.0.0, tested against 4.4.3) and no longer supports Zod 3. All schema introspection — schema relaxation for updates, MongoDB JSON Schema generation, denyUntrusted field extraction, and schema policy validation — was rewritten against Zod 4's internals (_zod.def, the new checks system, ZodPipe transform chains).

  • On Zod 3? Stay on the 1.x line — v1.1.0 backports everything below except the Zod 4 migration.
  • Apps must upgrade their own schemas to Zod 4. See the official Zod migration guide — notably .default() now short-circuits (use .prefault() for Zod 3 behavior), and z.record() requires an explicit key schema.
  • Public type signatures are preserved: ModelType, Selector, FieldsOf, ModelResultType, and the inferred input/output types of all CRUD and query methods work as before (verified by a compile-time type assertion suite).
  • Deprecated-but-supported Zod 4 APIs (z.string().email(), z.nativeEnum(), .passthrough()) continue to work in schemas passed to this package.
  • New Zod 4 string formats that carry a regex pattern (e.g. z.string().startsWith(), .lowercase()) are now representable in generated MongoDB JSON Schema validators.

This also resolves the typing collapse reported against v1.0.0 when Zod 4 was installed (withCommon returning any, insertAsync accepting any): v1.x's types referenced Zod 3's three-parameter ZodObject, which doesn't resolve against Zod 4's two-parameter signature.

✨ Features

Opt-in database-level validation via attachValidator: true:

const Users = new Model({
  name: 'users',
  schema: UserSchema,
  attachValidator: true,
});
  • Generates a MongoDB JSON Schema validator from your Zod schema and attaches it to the collection (createCollection for new collections, collMod for existing ones)
  • Defense-in-depth: writes that bypass Meteor entirely (rawCollection(), admin tools, other services) are still validated by MongoDB itself
  • Enforces validationLevel: 'strict' and validationAction: 'error'; server-only
  • Write methods await attachment internally, so there is no race between construction and the first operation
  • bypassSchema bypasses the database layer too, via bypassDocumentValidation: true

See Database-Level Validation in the README.

🐛 Bug Fixes

  • Marker leak: the internal _meteortypedmodelTrusted marker is no longer persisted into MongoDB documents. Deny rules (which strip it) only run for client-initiated writes, so every server-side insertAsync/updateAsync/upsertAsync was writing it into stored documents. It is now only added on the client. See the cleanup section below for removing residue from existing documents.
  • Unprojected query types (find(), findOne(), findOneAsync() without fields): now return the full document type. Previously the projection type parameter fell back to its FieldsOf constraint, resolving every field in the result type to never — silently removing type safety from unprojected queries. Projected queries still narrow to the selected fields.
  • Wrapping Meteor.users (#1): wrapping an existing collection typed independently of the schema did not compile (Collection's generic is invariant). The constructor's collection parameter is now Mongo.Collection<any>, as the API docs always stated; the Model's own schema typing governs everything from there. The README example was also fixed (z.string() for username violated the non-empty-string policy and threw at construction).
  • Zod peer check: relaxed from the over-strict 3.23.x pin to ^4.0.0 (matching the new requirement).

🧹 Cleaning up existing data (upgrading from v1.0.0)

The marker-leak fix stops new pollution, but documents written server-side under v1.0.0 still carry the stray _meteortypedmodelTrusted: true field. It is inert for reads, but you should sweep it out — especially before enabling attachValidator, whose generated validators reject unknown fields (additionalProperties: false) and would flag those documents.

Run once on the server, after upgrading:

import { Meteor } from 'meteor/meteor';
import { AllModels } from 'meteor/typed:model';

Meteor.startup(async () => {
  for (const model of AllModels) {
    const { modifiedCount } = await model.collection.rawCollection().updateMany(
      { _meteortypedmodelTrusted: { $exists: true } },
      { $unset: { _meteortypedmodelTrusted: '' } },
    );
    if (modifiedCount > 0) {
      console.log(`typed:model cleanup: removed marker from ${modifiedCount} ${model.name} documents`);
    }
  }
});

Or directly in the mongo shell, across all collections:

db.getCollectionNames().forEach((name) => {
  const res = db[name].updateMany(
    { _meteortypedmodelTrusted: { $exists: true } },
    { $unset: { _meteortypedmodelTrusted: '' } }
  );
  if (res.modifiedCount > 0) print(`${name}: ${res.modifiedCount} cleaned`);
});

The sweep is safe to run repeatedly (it matches nothing once clean), and safe even with attachValidator already enabled — MongoDB validates the resulting document, and removing the field is exactly what makes it conform. Note the marker also rode along in $set on updates and $setOnInsert on upserts, so any collection a Model has written to server-side may be affected, not just ones with inserts.

🧪 Testing

  • Client-side tests were silently not running (0 passing): the shared test entry point used dynamic import(), which on the client resolves over DDP after the test driver has finished. Split into per-architecture entry points with static imports.
  • New suites: database validation (attachValidator paths, both validation layers, marker-leak regressions) and existing-collection wrapping (Meteor.users, issue #1).
  • New compile-time type assertions locking in the public type contract: insertAsync doc parameter typing (rejects wrong/missing/unknown fields), updateAsync/upsertAsync return types, full-document unprojected results, projection narrowing, and withCommon output types.
  • Suite now at 144 tests (112 server + 32 client), run against real MongoDB and a real browser (Playwright).

Full Changelog: v1.0.0...v2.0.0

v1.1.0 — Database validation & fixes (Zod 3 line)

Choose a tag to compare

@copleykj copleykj released this 23 Jul 00:25

Maintenance release for the Zod 3 line (zod@^3.23.0). This is a feature-parity backport of the v2.0.0 improvements, minus the Zod 4 migration — if you are on Zod 4, use typed:model@2.x instead.

The 1.x branch is maintenance-only from here: bug fixes land on request, new feature development happens on main (2.x).

✨ Features

Opt-in database-level validation via attachValidator: true:

const Users = new Model({
  name: 'users',
  schema: UserSchema,
  attachValidator: true,
});
  • Generates a MongoDB JSON Schema validator from your Zod schema and attaches it to the collection (createCollection for new collections, collMod for existing ones)
  • Defense-in-depth: writes that bypass Meteor entirely (rawCollection(), admin tools, other services) are still validated by MongoDB itself
  • Enforces validationLevel: 'strict' and validationAction: 'error'; server-only
  • Write methods await attachment internally, so there is no race between construction and the first operation
  • bypassSchema bypasses the database layer too, via bypassDocumentValidation: true

See Database-Level Validation in the README.

🐛 Bug Fixes

  • Marker leak: the internal _meteortypedmodelTrusted marker is no longer persisted into MongoDB documents. Deny rules (which strip it) only run for client-initiated writes, so every server-side insertAsync/updateAsync/upsertAsync was writing it into stored documents. It is now only added on the client. If you are on v1.0.0, this fix alone is worth upgrading for — see the cleanup section below for removing residue from existing documents.
  • Unprojected query types (find(), findOne(), findOneAsync() without fields): now return the full document type. Previously the projection type parameter fell back to its FieldsOf constraint, resolving every field in the result type to never — silently removing type safety from unprojected queries. Projected queries still narrow to the selected fields.
  • Wrapping Meteor.users (#1): wrapping an existing collection typed independently of the schema did not compile (Collection's generic is invariant). The constructor's collection parameter is now Mongo.Collection<any>, as the API docs always stated; the Model's own schema typing governs everything from there. The README example was also fixed (z.string() for username violated the non-empty-string policy and threw at construction).
  • Zod peer check: relaxed from 3.23.x to ^3.23.0, so zod 3.24 no longer fails the version check.

🧹 Cleaning up existing data (upgrading from v1.0.0)

The marker-leak fix stops new pollution, but documents written server-side under v1.0.0 still carry the stray _meteortypedmodelTrusted: true field. It is inert for reads, but you should sweep it out — especially before enabling attachValidator, whose generated validators reject unknown fields (additionalProperties: false) and would flag those documents.

Run once on the server, after upgrading:

import { Meteor } from 'meteor/meteor';
import { AllModels } from 'meteor/typed:model';

Meteor.startup(async () => {
  for (const model of AllModels) {
    const { modifiedCount } = await model.collection.rawCollection().updateMany(
      { _meteortypedmodelTrusted: { $exists: true } },
      { $unset: { _meteortypedmodelTrusted: '' } },
    );
    if (modifiedCount > 0) {
      console.log(`typed:model cleanup: removed marker from ${modifiedCount} ${model.name} documents`);
    }
  }
});

Or directly in the mongo shell, across all collections:

db.getCollectionNames().forEach((name) => {
  const res = db[name].updateMany(
    { _meteortypedmodelTrusted: { $exists: true } },
    { $unset: { _meteortypedmodelTrusted: '' } }
  );
  if (res.modifiedCount > 0) print(`${name}: ${res.modifiedCount} cleaned`);
});

The sweep is safe to run repeatedly (it matches nothing once clean), and safe even with attachValidator already enabled — MongoDB validates the resulting document, and removing the field is exactly what makes it conform. Note the marker also rode along in $set on updates and $setOnInsert on upserts, so any collection a Model has written to server-side may be affected, not just ones with inserts.

🧪 Testing

  • Client-side tests were silently not running (0 passing): the shared test entry point used dynamic import(), which on the client resolves over DDP after the test driver has finished. Split into per-architecture entry points with static imports.
  • New suites: database validation (attachValidator paths, both validation layers, marker-leak regressions) and existing-collection wrapping (Meteor.users, issue #1).
  • New compile-time type assertions locking in the public type contract: insertAsync doc parameter typing, updateAsync/upsertAsync return types, full-document unprojected results, projection narrowing, and withCommon output types.
  • Suite now at 143 tests (111 server + 32 client), run against real MongoDB and a real browser (Playwright).

Full Changelog: v1.0.0...v1.1.0

v1.0.0

Choose a tag to compare

@copleykj copleykj released this 11 Nov 16:43
v1.0.0