Skip to content

Commit 5c72d3b

Browse files
committed
reject _id mutations in callback-based updates
Object-based updates reject _id via #toSetFields, but callback-based updates (update, updateAll, updateCount, upsert) bypassed that check. Add _id validation in #resolveUpdateDoc and the upsert callback path so u._id.set() throws the same error as object mutations.
1 parent e1f26b3 commit 5c72d3b

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

packages/2-mongo-family/5-query-builders/orm/src/collection.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,10 @@ class MongoCollectionImpl<
460460
if (typeof input.update === 'function') {
461461
const accessor = createFieldAccessor<TContract, ModelName>();
462462
const ops = input.update(accessor);
463+
const idOp = ops.find((op) => op.field === '_id');
464+
if (idOp) {
465+
throw new Error('Mutation payloads cannot modify `_id`');
466+
}
463467
const dotPathOp = ops.find((op) => op.field.includes('.'));
464468
if (dotPathOp) {
465469
throw new Error(
@@ -646,6 +650,10 @@ class MongoCollectionImpl<
646650
if (typeof dataOrCallback === 'function') {
647651
const accessor = createFieldAccessor<TContract, ModelName>();
648652
const ops = dataOrCallback(accessor);
653+
const idOp = ops.find((op) => op.field === '_id');
654+
if (idOp) {
655+
throw new Error('Mutation payloads cannot modify `_id`');
656+
}
649657
return compileFieldOperations(ops, (field, value, operator) =>
650658
this.#wrapFieldOpValue(field, value, operator),
651659
);

packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,38 @@ describe('MongoCollection write methods', () => {
10661066
}),
10671067
).rejects.toThrow('_id');
10681068
});
1069+
1070+
it('update() with callback throws when _id is targeted', async () => {
1071+
const executor = createMockExecutor();
1072+
const col = createMongoCollection(contract, 'User', executor);
1073+
await expect(
1074+
col.where(MongoFieldFilter.eq('_id', 'id-1')).update((u) => [u._id.set('new-id')]),
1075+
).rejects.toThrow('_id');
1076+
});
1077+
1078+
it('updateAll() with callback throws when _id is targeted', async () => {
1079+
const executor = createMockExecutor([{ _id: 'id-1' }]);
1080+
const col = createMongoCollection(contract, 'User', executor);
1081+
const result = col
1082+
.where(MongoFieldFilter.eq('_id', 'id-1'))
1083+
.updateAll((u: FieldAccessor<Contract, 'User'>) => [u._id.set('new-id')]);
1084+
await expect(async () => {
1085+
for await (const _ of result) {
1086+
/* drain */
1087+
}
1088+
}).rejects.toThrow('_id');
1089+
});
1090+
1091+
it('upsert() with callback throws when _id is targeted', async () => {
1092+
const executor = createMockExecutor();
1093+
const col = createMongoCollection(contract, 'User', executor);
1094+
await expect(
1095+
col.where(MongoFieldFilter.eq('email', 'a@b.c')).upsert({
1096+
create: defaultUserData,
1097+
update: (u) => [u._id.set('new-id')],
1098+
}),
1099+
).rejects.toThrow('_id');
1100+
});
10691101
});
10701102

10711103
describe('immutability', () => {

0 commit comments

Comments
 (0)