Skip to content

Commit 97db041

Browse files
fix: update operator $setOnInsert not working
1 parent 4b091bb commit 97db041

6 files changed

Lines changed: 72 additions & 14 deletions

File tree

src/schema/type-helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ type UpdateOperators<TSchema extends AnySchema, T extends KV<any, any>> = {
214214
$mul?: PrettyNonEmpty<MapKVUpdateEnhanced<T, NumericType>>;
215215
$rename?: PrettyNonEmpty<MapKVUpdateRename<T>>;
216216
$set?: PrettyNonEmpty<MapKVUpdate<T>>;
217-
$setOnInsert?: InferSchemaInput<TSchema>;
217+
$setOnInsert?: PrettyNonEmpty<MapKVUpdate<T>>;
218218
$unset?: PrettyNonEmpty<MapKVUpdateUnset<T>>;
219219
$addToSet?: PrettyNonEmpty<MapKVUpdateAddToSet<T>>;
220220
$pop?: PrettyNonEmpty<MapKVUpdateEnhanced<T, MonarchArray<any>, 1 | -1>>;

src/schema/update.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
MonarchNullable,
1212
MonarchNumber,
1313
MonarchOptional,
14+
MonarchObject,
1415
MonarchType,
1516
type AnyMonarchType,
1617
} from "../types";
@@ -83,7 +84,10 @@ export function updateParser<T extends AnyMonarchType>(
8384

8485
// Upsert update: parse and flatten full document
8586
if (setOnInsertSkipSet) {
86-
input.$setOnInsert = parseSetOnInsertOperator(schemaType, update.$setOnInsert ?? {}, setOnInsertSkipSet);
87+
const setOnInsert = parseSetOnInsertOperator(schemaType, update.$setOnInsert ?? {}, setOnInsertSkipSet);
88+
if (Object.keys(setOnInsert).length > 0) {
89+
input.$setOnInsert = setOnInsert;
90+
}
8791
}
8892

8993
return input;
@@ -104,7 +108,7 @@ function unwrapTo<T extends new (...args: any) => AnyMonarchType>(
104108
}
105109

106110
function parseFieldsOperator(
107-
op: "$set" | "$min" | "$max",
111+
op: "$set" | "$min" | "$max" | "$setOnInsert",
108112
schemaType: AnyMonarchType,
109113
fields: Record<string, unknown>,
110114
schemaUpdates?: Map<string, { op: string; value: any }>,
@@ -147,7 +151,7 @@ function parseArrayOperator(
147151
const elementType = MonarchArray.type(arrayType);
148152
const parser = MonarchType.parser(elementType, path);
149153
if (typeof value === "object" && value !== null && "$each" in value) {
150-
const ops = value as { $each: unknown[]; [k: string]: unknown };
154+
const ops = value as { $each: unknown[];[k: string]: unknown };
151155
parsed[path] = { ...ops, $each: ops.$each.map(parser) };
152156
} else {
153157
parsed[path] = parser(value);
@@ -390,8 +394,12 @@ function parseSetOnInsertOperator(
390394
setOnInsertSkipSet: Set<string>,
391395
) {
392396
try {
393-
const parser = MonarchType.parser(schemaType);
394-
return flattenObject(parser(fields), setOnInsertSkipSet);
397+
const flatFields = flattenObject(fields, setOnInsertSkipSet);
398+
const defaults = schemaType.getDefaults();
399+
const flatDefaults = defaults ? flattenObject(defaults, setOnInsertSkipSet) : {};
400+
401+
const combinedFields = { ...flatDefaults, ...flatFields };
402+
return parseFieldsOperator("$setOnInsert", schemaType, combinedFields, undefined, setOnInsertSkipSet);
395403
} catch (error) {
396404
throw MonarchParseError.fromCause({ path: "$setOnInsert", cause: error });
397405
}

src/types/object.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ export class MonarchObject<T extends Record<string, AnyMonarchType>> extends Mon
4242
return new MonarchObject(this.shape);
4343
}
4444

45+
public getDefaults(): any {
46+
const obj: any = {};
47+
for (const [key, type] of Object.entries(this.shape)) {
48+
const def = (type as AnyMonarchType).getDefaults();
49+
if (def !== undefined) obj[key] = def;
50+
}
51+
return Object.keys(obj).length > 0 ? obj : undefined;
52+
}
53+
4554
protected index(path: string[], depth: number): AnyMonarchType {
4655
if (depth === path.length - 1) return this;
4756
const key = path[depth + 1];

src/types/tuple.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ export class MonarchTuple<T extends [AnyMonarchType, ...AnyMonarchType[]]> exten
4949
return new MonarchTuple(this.types);
5050
}
5151

52+
public getDefaults(): any {
53+
const arr: any[] = [];
54+
for (const type of this.types) {
55+
const def = type.getDefaults();
56+
if (def === undefined) return undefined;
57+
arr.push(def);
58+
}
59+
return arr;
60+
}
61+
5262
protected index(path: string[], depth: number): AnyMonarchType {
5363
if (depth === path.length - 1) return this;
5464
const index = path[depth + 1];

src/types/type.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,15 @@ export abstract class MonarchType<TInput, TOutput extends TInput = TInput> {
216216
return defaulted(this, defaultInput as InferTypeInput<this> | (() => InferTypeInput<this>));
217217
}
218218

219+
/**
220+
* Evaluates and returns the default values for this type.
221+
* If the type is an object, it recursively evaluates all nested defaults.
222+
* Returns undefined if the type has no defaults.
223+
*/
224+
public getDefaults(): any {
225+
return undefined;
226+
}
227+
219228
/**
220229
* Validate input.
221230
*
@@ -309,6 +318,10 @@ export class MonarchNullable<T extends AnyMonarchType> extends MonarchType<
309318
return this instanceof target || MonarchType.isInstanceOf(this.type, target);
310319
}
311320

321+
public getDefaults(): any {
322+
return this.type.getDefaults();
323+
}
324+
312325
public static type<T extends AnyMonarchType>(nullable: MonarchNullable<T>): T {
313326
return nullable.type;
314327
}
@@ -354,6 +367,10 @@ export class MonarchOptional<T extends AnyMonarchType> extends MonarchType<
354367
return this instanceof target || MonarchType.isInstanceOf(this.type, target);
355368
}
356369

370+
public getDefaults(): any {
371+
return this.type.getDefaults();
372+
}
373+
357374
public static type<T extends AnyMonarchType>(optional: MonarchOptional<T>): T {
358375
return optional.type;
359376
}
@@ -409,6 +426,10 @@ export class MonarchDefaulted<T extends AnyMonarchType> extends MonarchType<
409426
return this instanceof target || MonarchType.isInstanceOf(this.type, target);
410427
}
411428

429+
public getDefaults(): any {
430+
return MonarchDefaulted.isDefaultFunction(this.defaultInput) ? this.defaultInput() : this.defaultInput;
431+
}
432+
412433
public static type<T extends AnyMonarchType>(defaulted: MonarchDefaulted<T>): T {
413434
return defaulted.type;
414435
}

tests/query/update.test.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
22
import { createDatabase, createSchema, defineSchemas } from "../../src";
3-
import { array, boolean, date, mixed, number, string } from "../../src/types";
3+
import { array, boolean, date, literal, mixed, number, object, string, tuple } from "../../src/types";
44
import { createMockDatabase, mockUsers } from "../mock";
55

66
describe("Update Operations", async () => {
@@ -799,6 +799,7 @@ describe("Update Operations", async () => {
799799
count: number(),
800800
tags: array(string()),
801801
alias: string().optional(),
802+
pair: tuple([string().default("X"), number().default(99)]).optional(),
802803
});
803804

804805
const db = createDatabase(client.db(), defineSchemas({ UpsertSchema }));
@@ -831,13 +832,22 @@ describe("Update Operations", async () => {
831832
expect(result?.count).toBe(0);
832833
});
833834

834-
it("$setOnInsert fields fails validation when upsert is true", async () => {
835-
await expect(
836-
db.collections.docs
837-
// @ts-expect-error
838-
.findOneAndUpdate({ name: "ghost" }, { $set: { name: "alice" }, $setOnInsert: { count: 1 } })
839-
.options({ upsert: true }),
840-
).rejects.toThrow("$setOnInsert.name: expected 'string' received 'undefined'");
835+
it("$setOnInsert allows partial fields and generates defaults when upsert is true", async () => {
836+
const result = await db.collections.docs
837+
.findOneAndUpdate({ name: "ghost" }, { $set: { name: "alice" }, $setOnInsert: { count: 1 } })
838+
.options({ upsert: true, returnDocument: "after" });
839+
840+
expect(result?.name).toBe("alice");
841+
expect(result?.count).toBe(1);
842+
});
843+
844+
it("$setOnInsert safely generates default structures for tuples when upserting", async () => {
845+
const result = await db.collections.docs
846+
.findOneAndUpdate({ name: "ghost" }, { $set: { name: "alice" }, $setOnInsert: { count: 1 } })
847+
.options({ upsert: true, returnDocument: "after" });
848+
849+
expect(result?.name).toBe("alice");
850+
expect(result?.pair).toEqual(["X", 99]);
841851
});
842852
});
843853
});

0 commit comments

Comments
 (0)