Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions packages/orm/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,9 +961,20 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}
}

// read pre-update entity with ids so that the caller can use it to identify
// the entity being updated, the read data is used as return value if no update
// is made to the entity
const thisEntity = await this.getEntityIds(kysely, model, combinedWhere);
if (!thisEntity) {
if (throwIfNotFound) {
throw createNotFoundError(model);
} else {
return null;
}
}

if (Object.keys(finalData).length === 0) {
// nothing to update, return the original filter so that caller can identify the entity
return combinedWhere;
return thisEntity;
}

Comment thread
ymc9 marked this conversation as resolved.
let needIdRead = false;
Expand Down Expand Up @@ -1000,7 +1011,6 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}

const updateFields: any = {};
let thisEntity: any = undefined;

for (const field in finalData) {
const fieldDef = this.requireField(model, field);
Expand All @@ -1010,24 +1020,14 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
if (!allowRelationUpdate) {
throw createNotSupportedError(`Relation update not allowed for field "${field}"`);
}
if (!thisEntity) {
thisEntity = await this.getEntityIds(kysely, model, combinedWhere);
if (!thisEntity) {
if (throwIfNotFound) {
throw createNotFoundError(model);
} else {
return null;
}
}
}
const parentUpdates = await this.processRelationUpdates(
kysely,
model,
field,
fieldDef,
thisEntity,
finalData[field],
throwIfNotFound,
!fieldDef.array && !fieldDef.optional, // throw if not found for non-optional to-one relations
);

if (Object.keys(parentUpdates).length > 0) {
Expand All @@ -1044,8 +1044,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}

if (!hasFieldUpdate) {
// nothing to update, return the filter so that the caller can identify the entity
return combinedWhere;
// nothing to update, return the existing entity
return thisEntity;
} else {
fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model);
const query = kysely
Expand Down
108 changes: 107 additions & 1 deletion tests/e2e/orm/client-api/upsert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('Client upsert tests', () => {
let client: ClientContract<typeof schema>;

beforeEach(async () => {
client = (await createTestClient(schema)) as any;
client = await createTestClient(schema);
});

afterEach(async () => {
Expand Down Expand Up @@ -69,4 +69,110 @@ describe('Client upsert tests', () => {
email: 'u1@test.com',
});
});

it('works with upsert with empty update payload', async () => {
// Test 1: Upsert with empty update should create new record when it doesn't exist
const created = await client.user.upsert({
where: { id: '1' },
create: {
id: '1',
email: 'u1@test.com',
name: 'John',
},
update: {},
select: { id: true, email: true, name: true },
});

expect(created).toMatchObject({
id: '1',
email: 'u1@test.com',
name: 'John',
});

// Verify the record was created
const fetchedAfterCreate = await client.user.findUnique({
where: { id: '1' },
select: { id: true, email: true, name: true },
});

expect(fetchedAfterCreate).toMatchObject({
id: '1',
email: 'u1@test.com',
name: 'John',
});

// Test 2: Upsert with empty update should return existing record unchanged
const result = await client.user.upsert({
where: { id: '1' },
create: {
id: '1',
email: 'u1@test.com',
name: 'Jane',
},
update: {},
select: { id: true, email: true, name: true },
});

expect(result).toMatchObject({
id: '1',
email: 'u1@test.com',
name: 'John', // Should remain unchanged
});

// Verify the record was not modified
const fetched = await client.user.findUnique({
where: { id: '1' },
select: { id: true, email: true, name: true },
});

expect(fetched).toMatchObject({
id: '1',
email: 'u1@test.com',
name: 'John',
});
});

it('works with upsert with empty create payload', async () => {
const db = await createTestClient(`
model User {
id String @id @default(cuid())
name String?
}
`);

// Test 1: First upsert should create the entity with empty data
const created = await db.user.upsert({
where: { id: '1' },
create: {},
update: { name: 'Updated' },
});

expect(created).toBeTruthy();

// Verify the record was created
await expect(db.user.findFirst()).resolves.toMatchObject(created);

// Test 2: Second upsert should update the existing entity
const updated = await db.user.upsert({
where: { id: created.id },
create: {},
update: { name: 'Updated' },
});

expect(updated).toMatchObject({
id: created.id,
name: 'Updated',
});

// Verify the record was updated
await expect(
db.user.findUnique({
where: { id: created.id },
}),
).resolves.toMatchObject({
name: 'Updated',
});

await db.$disconnect();
});
Comment thread
ymc9 marked this conversation as resolved.
});
20 changes: 16 additions & 4 deletions tests/e2e/orm/policy/migrated/nested-to-many.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ describe('Policy tests to-many', () => {
],
},
m3: {
create: { value: 0 },
create: { id: 'm3', value: 0 },
},
},
});
Expand All @@ -686,30 +686,42 @@ describe('Policy tests to-many', () => {
});
expect(r.m3).toBeNull();

// make m3 readable
await db.$unuseAll().m3.update({
where: { id: 'm3' },
data: { value: 2 },
});

const r1 = await db.m1.update({
where: { id: '1' },
include: { m3: true, m2: true },
data: {
m3: {
update: {
value: 2,
value: 3,
},
},
},
});
// m3 is ok now
expect(r1.m3.value).toBe(2);
expect(r1.m3.value).toBe(3);
// m2 got filtered
expect(r1.m2).toHaveLength(0);

// make m2#1 readable
await db.$unuseAll().m2.update({
where: { id: '1' },
data: { value: 2 },
});

const r2 = await db.m1.update({
where: { id: '1' },
select: { m2: true },
data: {
m2: {
update: {
where: { id: '1' },
data: { value: 2 },
data: { value: 3 },
},
},
},
Expand Down
Loading