Skip to content

Commit f3c6db8

Browse files
committed
resolve comments
1 parent e73085c commit f3c6db8

2 files changed

Lines changed: 87 additions & 27 deletions

File tree

packages/server/src/api/rpc/index.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,30 @@ import z from 'zod';
77
import { fromError } from 'zod-validation-error/v4';
88
import type { ApiHandler, LogConfig, RequestContext, Response } from '../../types';
99
import { getProcedureDef, mapProcedureArgs, PROCEDURE_ROUTE_PREFIXES } from '../common/procedures';
10-
11-
const TRANSACTION_ROUTE_PREFIX = '$transaction' as const;
1210
import { loggerSchema } from '../common/schemas';
1311
import { processSuperJsonRequestPayload, unmarshalQ } from '../common/utils';
1412
import { log, registerCustomSerializers } from '../utils';
1513

14+
const TRANSACTION_ROUTE_PREFIX = '$transaction' as const;
15+
const VALID_OPS = new Set([
16+
'create',
17+
'createMany',
18+
'createManyAndReturn',
19+
'upsert',
20+
'findFirst',
21+
'findUnique',
22+
'findMany',
23+
'aggregate',
24+
'groupBy',
25+
'count',
26+
'exists',
27+
'update',
28+
'updateMany',
29+
'updateManyAndReturn',
30+
'delete',
31+
'deleteMany',
32+
]);
33+
1634
registerCustomSerializers();
1735

1836
/**
@@ -77,6 +95,7 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
7795
return this.handleTransaction({
7896
client,
7997
method: method.toUpperCase(),
98+
type: op,
8099
requestBody,
81100
});
82101
}
@@ -198,38 +217,27 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
198217
private async handleTransaction({
199218
client,
200219
method,
220+
type,
201221
requestBody,
202222
}: {
203223
client: ClientContract<Schema>;
204224
method: string;
225+
type: string;
205226
requestBody?: unknown;
206227
}): Promise<Response> {
207228
if (method !== 'POST') {
208229
return this.makeBadInputErrorResponse('invalid request method, only POST is supported');
209230
}
210231

232+
if (type !== 'sequential') {
233+
return this.makeBadInputErrorResponse(`unsupported transaction type: ${type}`);
234+
}
235+
211236
if (!requestBody || !Array.isArray(requestBody) || requestBody.length === 0) {
212237
return this.makeBadInputErrorResponse('request body must be a non-empty array of operations');
213238
}
214239

215-
const VALID_OPS = new Set([
216-
'create',
217-
'createMany',
218-
'createManyAndReturn',
219-
'upsert',
220-
'findFirst',
221-
'findUnique',
222-
'findMany',
223-
'aggregate',
224-
'groupBy',
225-
'count',
226-
'exists',
227-
'update',
228-
'updateMany',
229-
'updateManyAndReturn',
230-
'delete',
231-
'deleteMany',
232-
]);
240+
const processedOps: Array<{ model: string; op: string; args: unknown }> = [];
233241

234242
for (let i = 0; i < requestBody.length; i++) {
235243
const item = requestBody[i];
@@ -252,16 +260,19 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
252260
if (itemArgs !== undefined && itemArgs !== null && typeof itemArgs !== 'object') {
253261
return this.makeBadInputErrorResponse(`operation at index ${i} has invalid "args" field`);
254262
}
255-
}
256263

257-
const promises = (requestBody as any[]).map((item) => {
258-
const model = lowerCaseFirst(item.model as string);
259-
const op = item.op as string;
260-
const args = item.args ?? {};
261-
return (client as any)[model][op](args);
262-
});
264+
const { result: processedArgs, error: argsError } = await this.processRequestPayload(itemArgs ?? {});
265+
if (argsError) {
266+
return this.makeBadInputErrorResponse(`operation at index ${i}: ${argsError}`);
267+
}
268+
processedOps.push({ model: lowerCaseFirst(itemModel), op: itemOp, args: processedArgs });
269+
}
263270

264271
try {
272+
const promises = processedOps.map(({ model, op, args }) => {
273+
return (client as any)[model][op](args);
274+
});
275+
265276
log(this.options.log, 'debug', () => `handling "$transaction" request with ${promises.length} operations`);
266277

267278
const clientResult = await client.$transaction(promises as any);

packages/server/test/api/rpc.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,55 @@ procedure echoOverview(o: Overview): Overview
950950
expect(r.error.message).toMatch(/validation error/i);
951951
});
952952

953+
it('deserializes SuperJSON-encoded args per operation', async () => {
954+
const handleRequest = makeHandler();
955+
956+
// Clean up
957+
await rawClient.post.deleteMany();
958+
await rawClient.user.deleteMany();
959+
960+
// Serialize args containing a Date so they need SuperJSON deserialization
961+
const publishedAt = new Date('2025-01-15T00:00:00.000Z');
962+
const serialized = SuperJSON.serialize({
963+
data: { id: 'txuser3', email: 'txuser3@abc.com' },
964+
});
965+
const serializedPost = SuperJSON.serialize({
966+
data: { id: 'txpost3', title: 'Dated Post', authorId: 'txuser3', publishedAt },
967+
});
968+
969+
const r = await handleRequest({
970+
method: 'post',
971+
path: '/$transaction/sequential',
972+
requestBody: [
973+
{
974+
model: 'User',
975+
op: 'create',
976+
args: { ...(serialized.json as any), meta: { serialization: serialized.meta } },
977+
},
978+
{
979+
model: 'Post',
980+
op: 'create',
981+
args: { ...(serializedPost.json as any), meta: { serialization: serializedPost.meta } },
982+
},
983+
],
984+
client: rawClient,
985+
});
986+
987+
expect(r.status).toBe(200);
988+
expect(r.data).toHaveLength(2);
989+
expect(r.data[0]).toMatchObject({ id: 'txuser3' });
990+
expect(r.data[1]).toMatchObject({ id: 'txpost3' });
991+
992+
// Verify the Date was stored correctly
993+
const post = await (rawClient as any).post.findUnique({ where: { id: 'txpost3' } });
994+
expect(post?.publishedAt instanceof Date).toBe(true);
995+
expect((post?.publishedAt as Date)?.toISOString()).toBe(publishedAt.toISOString());
996+
997+
// Clean up
998+
await rawClient.post.deleteMany();
999+
await rawClient.user.deleteMany();
1000+
});
1001+
9531002
it('rolls back all operations when one fails', async () => {
9541003
const handleRequest = makeHandler();
9551004

0 commit comments

Comments
 (0)