@sylphx/lens-server 2.13.1 → 2.14.0
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/dist/index.d.ts +5 -5
- package/dist/index.js +128 -225
- package/package.json +2 -2
- package/src/e2e/server.test.ts +61 -43
- package/src/handlers/framework.ts +25 -10
- package/src/handlers/http.ts +14 -4
- package/src/handlers/ws.ts +37 -29
- package/src/server/create.test.ts +311 -173
- package/src/server/create.ts +89 -309
- package/src/server/types.ts +5 -5
package/src/e2e/server.test.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it } from "bun:test";
|
|
11
|
-
import { entity, firstValueFrom, lens, mutation, query, t } from "@sylphx/lens-core";
|
|
11
|
+
import { entity, firstValueFrom, isError, isSnapshot, lens, mutation, query, t } 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";
|
|
@@ -60,8 +60,10 @@ describe("E2E - Basic Operations", () => {
|
|
|
60
60
|
|
|
61
61
|
const result = await firstValueFrom(server.execute({ path: "getUsers" }));
|
|
62
62
|
|
|
63
|
-
expect(result
|
|
64
|
-
|
|
63
|
+
expect(isSnapshot(result)).toBe(true);
|
|
64
|
+
if (isSnapshot(result)) {
|
|
65
|
+
expect(result.data).toEqual(mockUsers);
|
|
66
|
+
}
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
it("query with input", async () => {
|
|
@@ -86,8 +88,10 @@ describe("E2E - Basic Operations", () => {
|
|
|
86
88
|
}),
|
|
87
89
|
);
|
|
88
90
|
|
|
89
|
-
expect(result
|
|
90
|
-
|
|
91
|
+
expect(isSnapshot(result)).toBe(true);
|
|
92
|
+
if (isSnapshot(result)) {
|
|
93
|
+
expect(result.data).toEqual(mockUsers[0]);
|
|
94
|
+
}
|
|
91
95
|
});
|
|
92
96
|
|
|
93
97
|
it("mutation", async () => {
|
|
@@ -113,13 +117,15 @@ describe("E2E - Basic Operations", () => {
|
|
|
113
117
|
}),
|
|
114
118
|
);
|
|
115
119
|
|
|
116
|
-
expect(result
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
expect(isSnapshot(result)).toBe(true);
|
|
121
|
+
if (isSnapshot(result)) {
|
|
122
|
+
expect(result.data).toEqual({
|
|
123
|
+
id: "user-new",
|
|
124
|
+
name: "Charlie",
|
|
125
|
+
email: "charlie@example.com",
|
|
126
|
+
status: "offline",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
123
129
|
});
|
|
124
130
|
|
|
125
131
|
it("handles query errors", async () => {
|
|
@@ -140,9 +146,10 @@ describe("E2E - Basic Operations", () => {
|
|
|
140
146
|
}),
|
|
141
147
|
);
|
|
142
148
|
|
|
143
|
-
expect(result
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
expect(isError(result)).toBe(true);
|
|
150
|
+
if (isError(result)) {
|
|
151
|
+
expect(result.error).toBe("Query failed");
|
|
152
|
+
}
|
|
146
153
|
});
|
|
147
154
|
|
|
148
155
|
it("handles unknown operation", async () => {
|
|
@@ -155,8 +162,10 @@ describe("E2E - Basic Operations", () => {
|
|
|
155
162
|
}),
|
|
156
163
|
);
|
|
157
164
|
|
|
158
|
-
expect(result
|
|
159
|
-
|
|
165
|
+
expect(isError(result)).toBe(true);
|
|
166
|
+
if (isError(result)) {
|
|
167
|
+
expect(result.error).toContain("not found");
|
|
168
|
+
}
|
|
160
169
|
});
|
|
161
170
|
});
|
|
162
171
|
|
|
@@ -254,15 +263,17 @@ describe("E2E - Selection", () => {
|
|
|
254
263
|
}),
|
|
255
264
|
);
|
|
256
265
|
|
|
257
|
-
expect(result
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
+
expect(isSnapshot(result)).toBe(true);
|
|
267
|
+
if (isSnapshot(result)) {
|
|
268
|
+
// Should include id (always) and selected fields
|
|
269
|
+
expect(result.data).toEqual({
|
|
270
|
+
id: "user-1",
|
|
271
|
+
name: "Alice",
|
|
272
|
+
});
|
|
273
|
+
// Should not include unselected fields
|
|
274
|
+
expect((result.data as Record<string, unknown>).email).toBeUndefined();
|
|
275
|
+
expect((result.data as Record<string, unknown>).status).toBeUndefined();
|
|
276
|
+
}
|
|
266
277
|
});
|
|
267
278
|
|
|
268
279
|
it("includes id by default in selection", async () => {
|
|
@@ -286,10 +297,13 @@ describe("E2E - Selection", () => {
|
|
|
286
297
|
}),
|
|
287
298
|
);
|
|
288
299
|
|
|
289
|
-
expect(result
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
300
|
+
expect(isSnapshot(result)).toBe(true);
|
|
301
|
+
if (isSnapshot(result)) {
|
|
302
|
+
expect(result.data).toEqual({
|
|
303
|
+
id: "user-1",
|
|
304
|
+
email: "alice@example.com",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
293
307
|
});
|
|
294
308
|
});
|
|
295
309
|
|
|
@@ -351,15 +365,17 @@ describe("E2E - Entity Resolvers", () => {
|
|
|
351
365
|
}),
|
|
352
366
|
);
|
|
353
367
|
|
|
354
|
-
expect(result
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
368
|
+
expect(isSnapshot(result)).toBe(true);
|
|
369
|
+
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
|
+
}
|
|
363
379
|
});
|
|
364
380
|
|
|
365
381
|
it("handles DataLoader batching for entity resolvers", async () => {
|
|
@@ -414,10 +430,12 @@ describe("E2E - Entity Resolvers", () => {
|
|
|
414
430
|
}),
|
|
415
431
|
);
|
|
416
432
|
|
|
417
|
-
expect(result
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
433
|
+
expect(isSnapshot(result)).toBe(true);
|
|
434
|
+
if (isSnapshot(result)) {
|
|
435
|
+
// Resolvers are called - exact count depends on DataLoader batching behavior
|
|
436
|
+
expect(batchCallCount).toBeGreaterThanOrEqual(2);
|
|
437
|
+
expect(result.data).toHaveLength(2);
|
|
438
|
+
}
|
|
421
439
|
});
|
|
422
440
|
});
|
|
423
441
|
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
-
import { firstValueFrom } from "@sylphx/lens-core";
|
|
40
|
+
import { firstValueFrom, isError, isSnapshot } from "@sylphx/lens-core";
|
|
41
41
|
import type { LensServer } from "../server/create.js";
|
|
42
42
|
|
|
43
43
|
// =============================================================================
|
|
@@ -77,11 +77,16 @@ export function createServerClientProxy(server: LensServer): unknown {
|
|
|
77
77
|
const input = args[0];
|
|
78
78
|
const result = await firstValueFrom(server.execute({ path, input }));
|
|
79
79
|
|
|
80
|
-
if (result
|
|
81
|
-
throw result.error;
|
|
80
|
+
if (isError(result)) {
|
|
81
|
+
throw new Error(result.error);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
if (isSnapshot(result)) {
|
|
85
|
+
return result.data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ops message - shouldn't happen for one-shot calls
|
|
89
|
+
return null;
|
|
85
90
|
},
|
|
86
91
|
});
|
|
87
92
|
}
|
|
@@ -115,11 +120,16 @@ export async function handleWebQuery(
|
|
|
115
120
|
|
|
116
121
|
const result = await firstValueFrom(server.execute({ path, input }));
|
|
117
122
|
|
|
118
|
-
if (result
|
|
119
|
-
return Response.json({ error: result.error
|
|
123
|
+
if (isError(result)) {
|
|
124
|
+
return Response.json({ error: result.error }, { status: 400 });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isSnapshot(result)) {
|
|
128
|
+
return Response.json({ data: result.data });
|
|
120
129
|
}
|
|
121
130
|
|
|
122
|
-
|
|
131
|
+
// ops message - forward as-is
|
|
132
|
+
return Response.json(result);
|
|
123
133
|
} catch (error) {
|
|
124
134
|
return Response.json(
|
|
125
135
|
{ error: error instanceof Error ? error.message : "Unknown error" },
|
|
@@ -150,11 +160,16 @@ export async function handleWebMutation(
|
|
|
150
160
|
|
|
151
161
|
const result = await firstValueFrom(server.execute({ path, input }));
|
|
152
162
|
|
|
153
|
-
if (result
|
|
154
|
-
return Response.json({ error: result.error
|
|
163
|
+
if (isError(result)) {
|
|
164
|
+
return Response.json({ error: result.error }, { status: 400 });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (isSnapshot(result)) {
|
|
168
|
+
return Response.json({ data: result.data });
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
|
|
171
|
+
// ops message - forward as-is
|
|
172
|
+
return Response.json(result);
|
|
158
173
|
} catch (error) {
|
|
159
174
|
return Response.json(
|
|
160
175
|
{ error: error instanceof Error ? error.message : "Unknown error" },
|
package/src/handlers/http.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Works with Bun, Node (with adapter), Vercel, Cloudflare Workers.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { firstValueFrom } from "@sylphx/lens-core";
|
|
8
|
+
import { firstValueFrom, isError, isSnapshot } from "@sylphx/lens-core";
|
|
9
9
|
import type { LensServer } from "../server/create.js";
|
|
10
10
|
|
|
11
11
|
// =============================================================================
|
|
@@ -347,8 +347,8 @@ export function createHTTPHandler(
|
|
|
347
347
|
}),
|
|
348
348
|
);
|
|
349
349
|
|
|
350
|
-
if (result
|
|
351
|
-
return new Response(JSON.stringify({ error:
|
|
350
|
+
if (isError(result)) {
|
|
351
|
+
return new Response(JSON.stringify({ error: result.error }), {
|
|
352
352
|
status: 500,
|
|
353
353
|
headers: {
|
|
354
354
|
"Content-Type": "application/json",
|
|
@@ -357,7 +357,17 @@ export function createHTTPHandler(
|
|
|
357
357
|
});
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
if (isSnapshot(result)) {
|
|
361
|
+
return new Response(JSON.stringify({ data: result.data }), {
|
|
362
|
+
headers: {
|
|
363
|
+
"Content-Type": "application/json",
|
|
364
|
+
...baseHeaders,
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ops message - forward as-is
|
|
370
|
+
return new Response(JSON.stringify(result), {
|
|
361
371
|
headers: {
|
|
362
372
|
"Content-Type": "application/json",
|
|
363
373
|
...baseHeaders,
|
package/src/handlers/ws.ts
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
26
|
firstValueFrom,
|
|
27
|
+
isError,
|
|
28
|
+
isSnapshot,
|
|
27
29
|
type ReconnectMessage,
|
|
28
30
|
type ReconnectSubscription,
|
|
29
31
|
} from "@sylphx/lens-core";
|
|
@@ -272,20 +274,24 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
|
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
// Execute query first to get data
|
|
275
|
-
let
|
|
277
|
+
let resultData: unknown;
|
|
276
278
|
try {
|
|
277
|
-
result = await firstValueFrom(server.execute({ path: operation, input }));
|
|
279
|
+
const result = await firstValueFrom(server.execute({ path: operation, input }));
|
|
278
280
|
|
|
279
|
-
if (result
|
|
281
|
+
if (isError(result)) {
|
|
280
282
|
conn.ws.send(
|
|
281
283
|
JSON.stringify({
|
|
282
284
|
type: "error",
|
|
283
285
|
id,
|
|
284
|
-
error: { code: "EXECUTION_ERROR", message: result.error
|
|
286
|
+
error: { code: "EXECUTION_ERROR", message: result.error },
|
|
285
287
|
}),
|
|
286
288
|
);
|
|
287
289
|
return;
|
|
288
290
|
}
|
|
291
|
+
|
|
292
|
+
if (isSnapshot(result)) {
|
|
293
|
+
resultData = result.data;
|
|
294
|
+
}
|
|
289
295
|
} catch (error) {
|
|
290
296
|
conn.ws.send(
|
|
291
297
|
JSON.stringify({
|
|
@@ -298,7 +304,7 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
|
|
|
298
304
|
}
|
|
299
305
|
|
|
300
306
|
// Extract entities from result
|
|
301
|
-
const entities =
|
|
307
|
+
const entities = resultData ? extractEntities(resultData) : [];
|
|
302
308
|
|
|
303
309
|
// Check for duplicate subscription ID - cleanup old one first
|
|
304
310
|
const existingSub = conn.subscriptions.get(id);
|
|
@@ -329,7 +335,7 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
|
|
|
329
335
|
fields,
|
|
330
336
|
entityKeys: new Set(entities.map(({ entity, entityId }) => `${entity}:${entityId}`)),
|
|
331
337
|
cleanups: [],
|
|
332
|
-
lastData:
|
|
338
|
+
lastData: resultData,
|
|
333
339
|
};
|
|
334
340
|
|
|
335
341
|
// Register subscriptions with server for each entity
|
|
@@ -456,27 +462,29 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
|
|
|
456
462
|
}),
|
|
457
463
|
);
|
|
458
464
|
|
|
459
|
-
if (result
|
|
465
|
+
if (isError(result)) {
|
|
460
466
|
conn.ws.send(
|
|
461
467
|
JSON.stringify({
|
|
462
468
|
type: "error",
|
|
463
469
|
id: message.id,
|
|
464
|
-
error: { code: "EXECUTION_ERROR", message: result.error
|
|
470
|
+
error: { code: "EXECUTION_ERROR", message: result.error },
|
|
465
471
|
}),
|
|
466
472
|
);
|
|
467
473
|
return;
|
|
468
474
|
}
|
|
469
475
|
|
|
470
|
-
|
|
471
|
-
|
|
476
|
+
if (isSnapshot(result)) {
|
|
477
|
+
// Apply field selection if specified
|
|
478
|
+
const selected = message.fields ? applySelection(result.data, message.fields) : result.data;
|
|
472
479
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
+
conn.ws.send(
|
|
481
|
+
JSON.stringify({
|
|
482
|
+
type: "result",
|
|
483
|
+
id: message.id,
|
|
484
|
+
data: selected,
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
}
|
|
480
488
|
} catch (error) {
|
|
481
489
|
conn.ws.send(
|
|
482
490
|
JSON.stringify({
|
|
@@ -498,32 +506,32 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
|
|
|
498
506
|
}),
|
|
499
507
|
);
|
|
500
508
|
|
|
501
|
-
if (result
|
|
509
|
+
if (isError(result)) {
|
|
502
510
|
conn.ws.send(
|
|
503
511
|
JSON.stringify({
|
|
504
512
|
type: "error",
|
|
505
513
|
id: message.id,
|
|
506
|
-
error: { code: "EXECUTION_ERROR", message: result.error
|
|
514
|
+
error: { code: "EXECUTION_ERROR", message: result.error },
|
|
507
515
|
}),
|
|
508
516
|
);
|
|
509
517
|
return;
|
|
510
518
|
}
|
|
511
519
|
|
|
512
|
-
|
|
513
|
-
|
|
520
|
+
if (isSnapshot(result)) {
|
|
521
|
+
// Broadcast to all subscribers of affected entities
|
|
514
522
|
const entities = extractEntities(result.data);
|
|
515
523
|
for (const { entity, entityId, entityData } of entities) {
|
|
516
524
|
await server.broadcast(entity, entityId, entityData);
|
|
517
525
|
}
|
|
518
|
-
}
|
|
519
526
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
+
conn.ws.send(
|
|
528
|
+
JSON.stringify({
|
|
529
|
+
type: "result",
|
|
530
|
+
id: message.id,
|
|
531
|
+
data: result.data,
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
527
535
|
} catch (error) {
|
|
528
536
|
conn.ws.send(
|
|
529
537
|
JSON.stringify({
|