Skip to content

feat: Check required schema properties in upsert updates - #1104

Open
umeshravuru wants to merge 1 commit into
plexinc:mainfrom
umeshravuru:upsert-required-insert-properties
Open

umeshravuru wants to merge 1 commit into
plexinc:mainfrom
umeshravuru:upsert-required-insert-properties

Conversation

@umeshravuru

@umeshravuru umeshravuru commented Aug 29, 2026

Copy link
Copy Markdown

Fixes #1019

Problem

Model.upsert accepted update filters which, when the upsert inserted a new document, produced documents missing required schema properties. The compiler was silent and the operation only failed at runtime with a MongoDB document validation error:

const companySchema = schema({
  name: types.string({ required: true }),
  website: types.string({ required: true }),
});

// No TypeScript error before this change,
// but fails at runtime with "Document failed validation"
await companyModel.upsert({ website: 'example.com' }, { $set: {}, $setOnInsert: {} });

Solution

The update argument of upsert is now checked against a new PaprUpsertUpdateFilter type, which verifies that every required property in the schema is provided by at least one of the sources MongoDB and papr use to build the inserted document:

  • the filter equality fields (top-level keys, including inside top-level $and clauses; dot-notation keys count for their root property);
  • the update operators which create missing fields on insert ($set, $setOnInsert, $inc, $mul, $min, $max, $push, $addToSet, $currentDate, $bit);
  • the schema defaults (static and dynamic) and the timestamps options (reusing the existing DocumentForInsert type).

When required properties are missing, the type resolves to { $setOnInsert: { ...missing } }, so the compiler error names exactly the properties to add:

Property 'name' is missing in type '{}' but required in type '{ name: string; }'.

Design notes and limitations

  • Fields using comparison operators in the filter (e.g. { foo: { $gt: 1 } }) are treated as provided, because they cannot be reliably distinguished from direct object equality values at the type level. This keeps the check free of false positives, at the cost of not flagging some unsafe calls.
  • Fields inside $or/$nor clauses are not treated as provided (MongoDB does not copy them into the inserted document).
  • Non-literal filters and updates (values typed as PaprFilter<TSchema> / PaprUpdateFilter<TSchema>) skip the check, since their keys cannot be known statically.
  • updateMany with upsert: true in its options and bulkWrite update operations with upsert: true still accept unchecked updates — happy to tackle those in a follow-up if this approach is accepted.

Breaking change

Code which previously compiled but could insert invalid documents at runtime now fails to compile; the fix is to provide the missing properties in $setOnInsert (or $set). Marked as BREAKING CHANGE in the commit footer, matching how the previous type-strictness changes were released (e.g. #430 in v15.0.0).

Testing

  • pnpm test — 242 tests pass, including new tests in model.test.ts (call-site enforcement, including the exact No error when missing type with upsert #1019 repro schema) and mongodbTypes.test.ts (unit tests for the new type: per-operator behavior, $and/$or handling, dot-notation keys, defaults).
  • pnpm test:types, pnpm lint and pnpm pretty:ci are clean.
  • ./tests/run.sh — the full integration suite passes against a real MongoDB 6.0.16, and the TS fixtures compile against the built package with TypeScript 5.6. A new end-to-end case verifies that the new type error and MongoDB's runtime JSON schema validation reject the same upsert call.
  • Docs regenerated with node docs/build.js.

The update argument of Model.upsert is now type-checked to ensure the
document created by the insert branch of the upsert operation contains
all the required properties in the schema.

The required properties can be provided by the filter equality fields
(including inside top-level $and clauses), by the update operators
which create missing fields ($set, $setOnInsert, $inc, $push, etc.),
by the schema defaults or by the timestamps options. Any remaining
required properties must be passed in the $setOnInsert operator.

This matches the runtime behavior of the MongoDB JSON schema validation,
which rejects upserted documents with missing required properties.

Fixes plexinc#1019

BREAKING CHANGE: TypeScript compilation errors are now reported for
`Model.upsert` calls which may insert documents with missing required
properties. Previously such calls compiled without errors and failed at
runtime with a MongoDB document validation error. Provide the missing
properties in `$setOnInsert` (or `$set`) to fix the errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@umeshravuru
umeshravuru marked this pull request as ready for review August 29, 2026 21:57

@nunomarks nunomarks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR!

There were some regressions related to excess properties (and also bad names), and here are other findings: https://gist.github.com/nunomarks/bd0cc6d8c9bd776ca49bf3271e47a9ab

The false negatives might not be so easy to solve, so those are optional.

I'll take a closer look once these are addressed.

Comment thread src/mongodbTypes.ts
* 'nestedObject.direct' -> 'nestedObject'
* 'foo' -> 'foo'
*/
type RootProperty<Property> = Property extends `${infer Root}.${string}` ? Root : Property;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have this in src/DeepPick.ts (sort of) - HeadPaths. Please consolidate if possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No error when missing type with upsert

2 participants