@sylphx/lens-server 3.0.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-server",
3
- "version": "3.0.1",
3
+ "version": "4.0.1",
4
4
  "description": "Server runtime for Lens API framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,7 +30,7 @@
30
30
  "author": "SylphxAI",
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@sylphx/lens-core": "^3.0.1"
33
+ "@sylphx/lens-core": "^4.0.2"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "^5.9.3",
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { describe, expect, it } from "bun:test";
11
- import { entity, firstValueFrom, isError, isSnapshot, lens, mutation, query, t } from "@sylphx/lens-core";
11
+ import { firstValueFrom, id, isError, isSnapshot, model, mutation, query, resolver, string } from "@sylphx/lens-core";
12
12
  import { z } from "zod";
13
13
  import { optimisticPlugin } from "../plugin/optimistic.js";
14
14
  import { createApp } from "../server/create.js";
@@ -18,18 +18,18 @@ import { createApp } from "../server/create.js";
18
18
  // =============================================================================
19
19
 
20
20
  // Entities
21
- const User = entity("User", {
22
- id: t.id(),
23
- name: t.string(),
24
- email: t.string(),
25
- status: t.string(),
21
+ const User = model("User", {
22
+ id: id(),
23
+ name: string(),
24
+ email: string(),
25
+ status: string(),
26
26
  });
27
27
 
28
- const Post = entity("Post", {
29
- id: t.id(),
30
- title: t.string(),
31
- content: t.string(),
32
- authorId: t.string(),
28
+ const Post = model("Post", {
29
+ id: id(),
30
+ title: string(),
31
+ content: string(),
32
+ authorId: string(),
33
33
  });
34
34
 
35
35
  // Mock data
@@ -68,10 +68,10 @@ describe("E2E - Basic Operations", () => {
68
68
 
69
69
  it("query with input", async () => {
70
70
  const getUser = query()
71
- .input(z.object({ id: z.string() }))
71
+ .args(z.object({ id: z.string() }))
72
72
  .returns(User)
73
- .resolve(({ input }) => {
74
- const user = mockUsers.find((u) => u.id === input.id);
73
+ .resolve(({ args }) => {
74
+ const user = mockUsers.find((u) => u.id === args.id);
75
75
  if (!user) throw new Error("User not found");
76
76
  return user;
77
77
  });
@@ -96,12 +96,12 @@ describe("E2E - Basic Operations", () => {
96
96
 
97
97
  it("mutation", async () => {
98
98
  const createUser = mutation()
99
- .input(z.object({ name: z.string(), email: z.string() }))
99
+ .args(z.object({ name: z.string(), email: z.string() }))
100
100
  .returns(User)
101
- .resolve(({ input }) => ({
101
+ .resolve(({ args }) => ({
102
102
  id: "user-new",
103
- name: input.name,
104
- email: input.email,
103
+ name: args.name,
104
+ email: args.email,
105
105
  status: "offline",
106
106
  }));
107
107
 
@@ -130,7 +130,7 @@ describe("E2E - Basic Operations", () => {
130
130
 
131
131
  it("handles query errors", async () => {
132
132
  const failingQuery = query()
133
- .input(z.object({ id: z.string() }))
133
+ .args(z.object({ id: z.string() }))
134
134
  .resolve(() => {
135
135
  throw new Error("Query failed");
136
136
  });
@@ -178,7 +178,7 @@ describe("E2E - Context", () => {
178
178
  let capturedContext: unknown = null;
179
179
 
180
180
  const getUser = query()
181
- .input(z.object({ id: z.string() }))
181
+ .args(z.object({ id: z.string() }))
182
182
  .resolve(({ ctx }) => {
183
183
  capturedContext = ctx;
184
184
  return mockUsers[0];
@@ -206,7 +206,7 @@ describe("E2E - Context", () => {
206
206
  let capturedContext: unknown = null;
207
207
 
208
208
  const getUser = query()
209
- .input(z.object({ id: z.string() }))
209
+ .args(z.object({ id: z.string() }))
210
210
  .resolve(({ ctx }) => {
211
211
  capturedContext = ctx;
212
212
  return mockUsers[0];
@@ -240,10 +240,10 @@ describe("E2E - Context", () => {
240
240
  describe("E2E - Selection", () => {
241
241
  it("applies $select to filter fields", async () => {
242
242
  const getUser = query()
243
- .input(z.object({ id: z.string() }))
243
+ .args(z.object({ id: z.string() }))
244
244
  .returns(User)
245
- .resolve(({ input }) => {
246
- const user = mockUsers.find((u) => u.id === input.id);
245
+ .resolve(({ args }) => {
246
+ const user = mockUsers.find((u) => u.id === args.id);
247
247
  if (!user) throw new Error("User not found");
248
248
  return user;
249
249
  });
@@ -278,9 +278,9 @@ describe("E2E - Selection", () => {
278
278
 
279
279
  it("includes id by default in selection", async () => {
280
280
  const getUser = query()
281
- .input(z.object({ id: z.string() }))
281
+ .args(z.object({ id: z.string() }))
282
282
  .returns(User)
283
- .resolve(({ input }) => mockUsers.find((u) => u.id === input.id)!);
283
+ .resolve(({ args }) => mockUsers.find((u) => u.id === args.id)!);
284
284
 
285
285
  const server = createApp({
286
286
  entities: { User },
@@ -323,28 +323,36 @@ describe("E2E - Entity Resolvers", () => {
323
323
  { id: "post-2", title: "Second Post", content: "More content", authorId: "user-1" },
324
324
  ];
325
325
 
326
+ // Define User model
327
+ const UserWithPosts = model("UserWithPosts", {
328
+ id: id(),
329
+ name: string(),
330
+ email: string(),
331
+ });
332
+
333
+ // Define resolver with posts relation (new API)
334
+ const userResolver = resolver(UserWithPosts, (t) => ({
335
+ id: t.expose("id"),
336
+ name: t.expose("name"),
337
+ email: t.expose("email"),
338
+ // Plain function for relations
339
+ posts: ({ source }) => posts.filter((p) => p.authorId === source.id),
340
+ }));
341
+
326
342
  const getUser = query()
327
- .input(z.object({ id: z.string() }))
328
- .returns(User)
329
- .resolve(({ input }) => {
330
- const user = users.find((u) => u.id === input.id);
343
+ .args(z.object({ id: z.string() }))
344
+ .returns(UserWithPosts)
345
+ .resolve(({ args }) => {
346
+ const user = users.find((u) => u.id === args.id);
331
347
  if (!user) throw new Error("Not found");
332
348
  return user;
333
349
  });
334
350
 
335
- // Create entity resolvers using lens() factory
336
- const { resolver } = lens();
337
- const userResolver = resolver(User, (f) => ({
338
- id: f.expose("id"),
339
- name: f.expose("name"),
340
- email: f.expose("email"),
341
- posts: f.many(Post).resolve(({ parent }) => posts.filter((p) => p.authorId === parent.id)),
342
- }));
343
-
344
351
  const server = createApp({
345
- entities: { User, Post },
352
+ entities: { UserWithPosts, Post },
346
353
  queries: { getUser },
347
354
  resolvers: [userResolver],
355
+ context: () => ({}),
348
356
  });
349
357
 
350
358
  // Test with $select for nested posts
@@ -367,14 +375,10 @@ describe("E2E - Entity Resolvers", () => {
367
375
 
368
376
  expect(isSnapshot(result)).toBe(true);
369
377
  if (isSnapshot(result)) {
370
- expect(result.data).toMatchObject({
371
- id: "user-1",
372
- name: "Alice",
373
- posts: [
374
- { id: "post-1", title: "Hello World" },
375
- { id: "post-2", title: "Second Post" },
376
- ],
377
- });
378
+ // Verify base fields and posts work with new resolver() API
379
+ expect(result.data).toHaveProperty("name", "Alice");
380
+ expect((result.data as any).posts).toHaveLength(2);
381
+ expect((result.data as any).posts[0]).toHaveProperty("title", "Hello World");
378
382
  }
379
383
  });
380
384
 
@@ -392,25 +396,32 @@ describe("E2E - Entity Resolvers", () => {
392
396
  { id: "post-2", title: "Post 2", authorId: "user-2" },
393
397
  ];
394
398
 
395
- const getUsers = query()
396
- .returns([User])
397
- .resolve(() => users);
399
+ // Define User model
400
+ const UserBatched = model("UserBatched", {
401
+ id: id(),
402
+ name: string(),
403
+ });
398
404
 
399
- // Create entity resolvers using lens() factory
400
- const { resolver } = lens();
401
- const userResolver = resolver(User, (f) => ({
402
- id: f.expose("id"),
403
- name: f.expose("name"),
404
- posts: f.many(Post).resolve(({ parent }) => {
405
+ // Define resolver with posts relation (new API)
406
+ const userResolver = resolver(UserBatched, (t) => ({
407
+ id: t.expose("id"),
408
+ name: t.expose("name"),
409
+ // Plain function for relations
410
+ posts: ({ source }) => {
405
411
  batchCallCount++;
406
- return posts.filter((p) => p.authorId === parent.id);
407
- }),
412
+ return posts.filter((p) => p.authorId === source.id);
413
+ },
408
414
  }));
409
415
 
416
+ const getUsers = query()
417
+ .returns([UserBatched])
418
+ .resolve(() => users);
419
+
410
420
  const server = createApp({
411
- entities: { User, Post },
421
+ entities: { UserBatched, Post },
412
422
  queries: { getUsers },
413
423
  resolvers: [userResolver],
424
+ context: () => ({}),
414
425
  });
415
426
 
416
427
  // Execute query with nested selection for all users
@@ -432,9 +443,12 @@ describe("E2E - Entity Resolvers", () => {
432
443
 
433
444
  expect(isSnapshot(result)).toBe(true);
434
445
  if (isSnapshot(result)) {
435
- // Resolvers are called - exact count depends on DataLoader batching behavior
446
+ // Should batch calls - with DataLoader we get 2 calls (one per user)
436
447
  expect(batchCallCount).toBeGreaterThanOrEqual(2);
437
448
  expect(result.data).toHaveLength(2);
449
+ // Verify posts are resolved
450
+ expect((result.data as any)[0].posts).toHaveLength(1);
451
+ expect((result.data as any)[1].posts).toHaveLength(1);
438
452
  }
439
453
  });
440
454
  });
@@ -446,14 +460,14 @@ describe("E2E - Entity Resolvers", () => {
446
460
  describe("E2E - Metadata", () => {
447
461
  it("returns correct metadata structure", () => {
448
462
  const getUser = query()
449
- .input(z.object({ id: z.string() }))
463
+ .args(z.object({ id: z.string() }))
450
464
  .returns(User)
451
- .resolve(({ input }) => mockUsers.find((u) => u.id === input.id)!);
465
+ .resolve(({ args }) => mockUsers.find((u) => u.id === args.id)!);
452
466
 
453
467
  const createUser = mutation()
454
- .input(z.object({ name: z.string() }))
468
+ .args(z.object({ name: z.string() }))
455
469
  .returns(User)
456
- .resolve(({ input }) => ({ id: "new", name: input.name, email: "", status: "" }));
470
+ .resolve(({ args }) => ({ id: "new", name: args.name, email: "", status: "" }));
457
471
 
458
472
  const server = createApp({
459
473
  entities: { User },
@@ -475,12 +489,12 @@ describe("E2E - Metadata", () => {
475
489
 
476
490
  it("auto-derives optimistic hints from naming with plugin", () => {
477
491
  const updateUser = mutation()
478
- .input(z.object({ id: z.string(), name: z.string() }))
492
+ .args(z.object({ id: z.string(), name: z.string() }))
479
493
  .returns(User)
480
- .resolve(({ input }) => ({ ...mockUsers[0], name: input.name }));
494
+ .resolve(({ args }) => ({ ...mockUsers[0], name: args.name }));
481
495
 
482
496
  const deleteUser = mutation()
483
- .input(z.object({ id: z.string() }))
497
+ .args(z.object({ id: z.string() }))
484
498
  .resolve(() => ({ success: true }));
485
499
 
486
500
  const server = createApp({
@@ -196,45 +196,78 @@ export function handleWebSSE(
196
196
  signal?: AbortSignal,
197
197
  ): Response {
198
198
  const inputParam = url.searchParams.get("input");
199
- const input = inputParam ? JSON.parse(inputParam) : undefined;
199
+
200
+ // Parse input with error handling
201
+ let input: unknown;
202
+ if (inputParam) {
203
+ try {
204
+ input = JSON.parse(inputParam);
205
+ } catch (parseError) {
206
+ // Return error response for malformed JSON
207
+ const encoder = new TextEncoder();
208
+ const errorStream = new ReadableStream({
209
+ start(controller) {
210
+ const errMsg = parseError instanceof Error ? parseError.message : "Invalid JSON";
211
+ const data = `event: error\ndata: ${JSON.stringify({ error: `Invalid input JSON: ${errMsg}` })}\n\n`;
212
+ controller.enqueue(encoder.encode(data));
213
+ controller.close();
214
+ },
215
+ });
216
+ return new Response(errorStream, {
217
+ headers: {
218
+ "Content-Type": "text/event-stream",
219
+ "Cache-Control": "no-cache",
220
+ Connection: "keep-alive",
221
+ },
222
+ });
223
+ }
224
+ }
200
225
 
201
226
  const stream = new ReadableStream({
202
227
  start(controller) {
203
228
  const encoder = new TextEncoder();
204
229
 
205
- const result = server.execute({ path, input });
206
-
207
- if (result && typeof result === "object" && "subscribe" in result) {
208
- const observable = result as {
209
- subscribe: (handlers: {
210
- next: (value: { data?: unknown }) => void;
211
- error: (err: Error) => void;
212
- complete: () => void;
213
- }) => { unsubscribe: () => void };
214
- };
215
-
216
- const subscription = observable.subscribe({
217
- next: (value) => {
218
- const data = `data: ${JSON.stringify(value.data)}\n\n`;
219
- controller.enqueue(encoder.encode(data));
220
- },
221
- error: (err) => {
222
- const data = `event: error\ndata: ${JSON.stringify({ error: err.message })}\n\n`;
223
- controller.enqueue(encoder.encode(data));
224
- controller.close();
225
- },
226
- complete: () => {
227
- controller.close();
228
- },
229
- });
230
-
231
- // Clean up on abort
232
- if (signal) {
233
- signal.addEventListener("abort", () => {
234
- subscription.unsubscribe();
235
- controller.close();
230
+ try {
231
+ const result = server.execute({ path, input });
232
+
233
+ if (result && typeof result === "object" && "subscribe" in result) {
234
+ const observable = result as {
235
+ subscribe: (handlers: {
236
+ next: (value: { data?: unknown }) => void;
237
+ error: (err: Error) => void;
238
+ complete: () => void;
239
+ }) => { unsubscribe: () => void };
240
+ };
241
+
242
+ const subscription = observable.subscribe({
243
+ next: (value) => {
244
+ const data = `data: ${JSON.stringify(value.data)}\n\n`;
245
+ controller.enqueue(encoder.encode(data));
246
+ },
247
+ error: (err) => {
248
+ const data = `event: error\ndata: ${JSON.stringify({ error: err.message })}\n\n`;
249
+ controller.enqueue(encoder.encode(data));
250
+ controller.close();
251
+ },
252
+ complete: () => {
253
+ controller.close();
254
+ },
236
255
  });
256
+
257
+ // Clean up on abort
258
+ if (signal) {
259
+ signal.addEventListener("abort", () => {
260
+ subscription.unsubscribe();
261
+ controller.close();
262
+ });
263
+ }
237
264
  }
265
+ } catch (execError) {
266
+ // Handle synchronous errors from server.execute() or subscribe()
267
+ const errMsg = execError instanceof Error ? execError.message : "Internal error";
268
+ const data = `event: error\ndata: ${JSON.stringify({ error: errMsg })}\n\n`;
269
+ controller.enqueue(encoder.encode(data));
270
+ controller.close();
238
271
  }
239
272
  },
240
273
  });
@@ -13,17 +13,17 @@ import { createHTTPHandler } from "./http.js";
13
13
  // =============================================================================
14
14
 
15
15
  const getUser = query()
16
- .input(z.object({ id: z.string() }))
17
- .resolve(({ input }) => ({
18
- id: input.id,
16
+ .args(z.object({ id: z.string() }))
17
+ .resolve(({ args }) => ({
18
+ id: args.id,
19
19
  name: "Test User",
20
20
  }));
21
21
 
22
22
  const createUser = mutation()
23
- .input(z.object({ name: z.string() }))
24
- .resolve(({ input }) => ({
23
+ .args(z.object({ name: z.string() }))
24
+ .resolve(({ args }) => ({
25
25
  id: "new-id",
26
- name: input.name,
26
+ name: args.name,
27
27
  }));
28
28
 
29
29
  // =============================================================================
@@ -74,7 +74,7 @@ describe("createHTTPHandler", () => {
74
74
  method: "POST",
75
75
  headers: { "Content-Type": "application/json" },
76
76
  body: JSON.stringify({
77
- operation: "getUser",
77
+ path: "getUser",
78
78
  input: { id: "123" },
79
79
  }),
80
80
  });
@@ -97,7 +97,7 @@ describe("createHTTPHandler", () => {
97
97
  method: "POST",
98
98
  headers: { "Content-Type": "application/json" },
99
99
  body: JSON.stringify({
100
- operation: "createUser",
100
+ path: "createUser",
101
101
  input: { name: "New User" },
102
102
  }),
103
103
  });
@@ -128,7 +128,7 @@ describe("createHTTPHandler", () => {
128
128
  method: "POST",
129
129
  headers: { "Content-Type": "application/json" },
130
130
  body: JSON.stringify({
131
- operation: "getUser",
131
+ path: "getUser",
132
132
  input: { id: "456" },
133
133
  }),
134
134
  });
@@ -234,7 +234,7 @@ describe("createHTTPHandler", () => {
234
234
  method: "POST",
235
235
  headers: { "Content-Type": "application/json" },
236
236
  body: JSON.stringify({
237
- operation: "unknownOperation",
237
+ path: "unknownOperation",
238
238
  input: {},
239
239
  }),
240
240
  });
@@ -314,7 +314,7 @@ export function createHTTPHandler(
314
314
  (pathname === operationPath || pathname === `${pathPrefix}/`)
315
315
  ) {
316
316
  // Parse JSON body with proper error handling
317
- let body: { operation?: string; path?: string; input?: unknown };
317
+ let body: { path?: string; input?: unknown };
318
318
  try {
319
319
  body = (await request.json()) as typeof body;
320
320
  } catch {
@@ -328,9 +328,7 @@ export function createHTTPHandler(
328
328
  }
329
329
 
330
330
  try {
331
- // Support both 'operation' and 'path' for backwards compatibility
332
- const operationPath = body.operation ?? body.path;
333
- if (!operationPath) {
331
+ if (!body.path) {
334
332
  return new Response(JSON.stringify({ error: "Missing operation path" }), {
335
333
  status: 400,
336
334
  headers: {
@@ -342,7 +340,7 @@ export function createHTTPHandler(
342
340
 
343
341
  const result = await firstValueFrom(
344
342
  server.execute({
345
- path: operationPath,
343
+ path: body.path,
346
344
  input: body.input,
347
345
  }),
348
346
  );
@@ -16,6 +16,7 @@ export interface WSHandlerOptions {
16
16
  * Logger for debugging.
17
17
  */
18
18
  logger?: {
19
+ debug?: (message: string, ...args: unknown[]) => void;
19
20
  info?: (message: string, ...args: unknown[]) => void;
20
21
  warn?: (message: string, ...args: unknown[]) => void;
21
22
  error?: (message: string, ...args: unknown[]) => void;
@@ -55,9 +55,9 @@ function wait(ms = 10): Promise<void> {
55
55
  // =============================================================================
56
56
 
57
57
  const getUser = query()
58
- .input(z.object({ id: z.string() }))
59
- .resolve(({ input }) => ({
60
- id: input.id,
58
+ .args(z.object({ id: z.string() }))
59
+ .resolve(({ args }) => ({
60
+ id: args.id,
61
61
  name: "Test User",
62
62
  __typename: "User",
63
63
  }));
@@ -68,17 +68,17 @@ const listUsers = query().resolve(() => [
68
68
  ]);
69
69
 
70
70
  const createUser = mutation()
71
- .input(z.object({ name: z.string() }))
72
- .resolve(({ input }) => ({
71
+ .args(z.object({ name: z.string() }))
72
+ .resolve(({ args }) => ({
73
73
  id: "new-id",
74
- name: input.name,
74
+ name: args.name,
75
75
  __typename: "User",
76
76
  }));
77
77
 
78
78
  const slowQuery = query()
79
- .input(z.object({ delay: z.number() }))
80
- .resolve(async ({ input }) => {
81
- await new Promise((r) => setTimeout(r, input.delay));
79
+ .args(z.object({ delay: z.number() }))
80
+ .resolve(async ({ args }) => {
81
+ await new Promise((r) => setTimeout(r, args.delay));
82
82
  return { done: true };
83
83
  });
84
84
 
@@ -172,8 +172,13 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
172
172
  error: { code, message },
173
173
  }),
174
174
  );
175
- } catch {
176
- // Connection may already be closed
175
+ } catch (sendError) {
176
+ // Log actual error - don't silently swallow
177
+ // Common case is "connection already closed" but could be serialization failure
178
+ logger.debug?.(
179
+ `Failed to send error to client ${conn.id}:`,
180
+ sendError instanceof Error ? sendError.message : String(sendError),
181
+ );
177
182
  }
178
183
  }
179
184
 
@@ -513,7 +518,13 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
513
518
 
514
519
  // Notify plugins of field updates
515
520
  for (const entityKey of sub.entityKeys) {
516
- const [entity, entityId] = entityKey.split(":");
521
+ const parts = entityKey.split(":");
522
+ // Validate entityKey format (must be "Entity:id")
523
+ if (parts.length < 2) {
524
+ logger.warn?.(`Invalid entityKey format: "${entityKey}" (expected "Entity:id")`);
525
+ continue;
526
+ }
527
+ const [entity, entityId] = parts;
517
528
  await pluginManager.runOnUpdateFields({
518
529
  clientId: conn.id,
519
530
  subscriptionId: sub.id,
package/src/index.ts CHANGED
@@ -45,8 +45,6 @@ export {
45
45
  type QueryDef,
46
46
  // Operations
47
47
  query,
48
- type ResolverContext,
49
- type ResolverFn,
50
48
  type RouterDef,
51
49
  type RouterRoutes,
52
50
  router,
@@ -17,7 +17,7 @@
17
17
  * .input(z.object({ id: z.string(), name: z.string() }))
18
18
  * .returns(User)
19
19
  * .optimistic('merge') // ✅ Type-safe
20
- * .resolve(({ input }) => db.user.update(input));
20
+ * .resolve(({ args }) => db.user.update(args));
21
21
  *
22
22
  * const server = createApp({ router, plugins });
23
23
  * ```
@@ -151,7 +151,7 @@ interface ReifyPipeline {
151
151
  * Sugar syntax:
152
152
  * - "merge" → entity.update with input fields
153
153
  * - "create" → entity.create from output
154
- * - "delete" → entity.delete by input.id
154
+ * - "delete" → entity.delete by args.id
155
155
  *
156
156
  * Returns the original value if already a Pipeline.
157
157
  *
@@ -176,7 +176,7 @@ function sugarToPipeline(
176
176
 
177
177
  switch (sugar) {
178
178
  case "merge": {
179
- // entity.update('Entity', { id: input.id, ...fields })
179
+ // entity.update('Entity', { id: args.id, ...fields })
180
180
  const updateData: Record<string, unknown> = {
181
181
  type: entity,
182
182
  id: $input("id"),
@@ -217,14 +217,14 @@ function sugarToPipeline(
217
217
  return pipeline as unknown as Pipeline;
218
218
  }
219
219
  case "delete": {
220
- // entity.delete('Entity', { id: input.id })
220
+ // entity.delete('Entity', { id: args.id })
221
221
  const pipeline: ReifyPipeline = {
222
222
  $pipe: [
223
223
  {
224
224
  $do: "entity.delete",
225
225
  $with: {
226
226
  type: entity,
227
- id: { id: $input("id") },
227
+ id: $input("id"), // Fixed: was incorrectly nested as { id: $input("id") }
228
228
  },
229
229
  $as: "result",
230
230
  },
@@ -298,7 +298,7 @@ export type OptimisticPlugin = OptimisticPluginMarker & ServerPlugin;
298
298
  * .input(z.object({ id: z.string(), name: z.string() }))
299
299
  * .returns(User)
300
300
  * .optimistic('merge')
301
- * .resolve(({ input }) => db.user.update(input));
301
+ * .resolve(({ args }) => db.user.update(args));
302
302
  *
303
303
  * const server = createApp({ router, plugins });
304
304
  * ```