@sylphx/lens-server 4.0.0 → 4.1.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/README.md +76 -160
- package/dist/index.d.ts +138 -237
- package/dist/index.js +178 -313
- package/package.json +2 -2
- package/src/e2e/server.test.ts +12 -12
- package/src/handlers/http.test.ts +2 -2
- package/src/handlers/index.ts +3 -20
- package/src/handlers/ws.test.ts +3 -3
- package/src/index.ts +25 -41
- package/src/server/create.test.ts +34 -34
- package/src/server/create.ts +143 -14
- package/src/server/types.ts +34 -8
package/src/handlers/index.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @sylphx/lens-server - Handlers
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Streaming handlers for real-time communication.
|
|
5
|
+
* HTTP is handled by app.fetch directly.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
// =============================================================================
|
|
8
|
-
//
|
|
9
|
-
// =============================================================================
|
|
10
|
-
|
|
11
|
-
export { createHandler, type Handler, type HandlerOptions } from "./unified.js";
|
|
12
|
-
|
|
13
|
-
// =============================================================================
|
|
14
|
-
// Framework Handler Utilities
|
|
9
|
+
// Framework Handler Utilities (for framework packages)
|
|
15
10
|
// =============================================================================
|
|
16
11
|
|
|
17
12
|
export {
|
|
@@ -34,18 +29,6 @@ export {
|
|
|
34
29
|
type SSEHandlerConfig as SSEHandlerOptions,
|
|
35
30
|
} from "../sse/handler.js";
|
|
36
31
|
|
|
37
|
-
// =============================================================================
|
|
38
|
-
// HTTP Handler
|
|
39
|
-
// =============================================================================
|
|
40
|
-
|
|
41
|
-
export {
|
|
42
|
-
createHTTPHandler,
|
|
43
|
-
type HealthCheckOptions,
|
|
44
|
-
type HealthCheckResponse,
|
|
45
|
-
type HTTPHandler,
|
|
46
|
-
type HTTPHandlerOptions,
|
|
47
|
-
} from "./http.js";
|
|
48
|
-
|
|
49
32
|
// =============================================================================
|
|
50
33
|
// WebSocket Handler
|
|
51
34
|
// =============================================================================
|
package/src/handlers/ws.test.ts
CHANGED
|
@@ -55,7 +55,7 @@ function wait(ms = 10): Promise<void> {
|
|
|
55
55
|
// =============================================================================
|
|
56
56
|
|
|
57
57
|
const getUser = query()
|
|
58
|
-
.
|
|
58
|
+
.args(z.object({ id: z.string() }))
|
|
59
59
|
.resolve(({ args }) => ({
|
|
60
60
|
id: args.id,
|
|
61
61
|
name: "Test User",
|
|
@@ -68,7 +68,7 @@ const listUsers = query().resolve(() => [
|
|
|
68
68
|
]);
|
|
69
69
|
|
|
70
70
|
const createUser = mutation()
|
|
71
|
-
.
|
|
71
|
+
.args(z.object({ name: z.string() }))
|
|
72
72
|
.resolve(({ args }) => ({
|
|
73
73
|
id: "new-id",
|
|
74
74
|
name: args.name,
|
|
@@ -76,7 +76,7 @@ const createUser = mutation()
|
|
|
76
76
|
}));
|
|
77
77
|
|
|
78
78
|
const slowQuery = query()
|
|
79
|
-
.
|
|
79
|
+
.args(z.object({ delay: z.number() }))
|
|
80
80
|
.resolve(async ({ args }) => {
|
|
81
81
|
await new Promise((r) => setTimeout(r, args.delay));
|
|
82
82
|
return { done: true };
|
package/src/index.ts
CHANGED
|
@@ -3,33 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Server runtime for Lens API framework.
|
|
5
5
|
*
|
|
6
|
-
* Architecture:
|
|
7
|
-
* - App = Executor with optional plugin support
|
|
8
|
-
* - Stateless (default): Pure executor
|
|
9
|
-
* - Stateful (with opLog): Cursor-based state synchronization
|
|
10
|
-
* - Handlers = Pure protocol handlers (HTTP, WebSocket, SSE)
|
|
11
|
-
* - No business logic - just translate protocol to app calls
|
|
12
|
-
* - Plugins = App-level middleware (opLog, auth, logger)
|
|
13
|
-
* - Configured at app level, not handler level
|
|
14
|
-
*
|
|
15
6
|
* @example
|
|
16
7
|
* ```typescript
|
|
17
|
-
* // Stateless mode (default)
|
|
18
|
-
* const app = createApp({ router });
|
|
19
|
-
* const wsHandler = createWSHandler(app);
|
|
20
|
-
*
|
|
21
|
-
* // With opLog plugin (cursor-based state sync)
|
|
22
8
|
* const app = createApp({
|
|
23
|
-
* router,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
9
|
+
* router: appRouter,
|
|
10
|
+
* entities: { User, Post },
|
|
11
|
+
* resolvers: [userResolver, postResolver],
|
|
12
|
+
* context: () => ({ db }),
|
|
13
|
+
* })
|
|
26
14
|
*
|
|
27
|
-
* //
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* plugins: [opLog({ storage: upstashStorage({ redis }) })],
|
|
32
|
-
* });
|
|
15
|
+
* // App is directly callable - works with any runtime
|
|
16
|
+
* Bun.serve({ fetch: app })
|
|
17
|
+
* Deno.serve(app)
|
|
18
|
+
* export default app // Cloudflare Workers
|
|
33
19
|
* ```
|
|
34
20
|
*/
|
|
35
21
|
|
|
@@ -91,36 +77,34 @@ export {
|
|
|
91
77
|
} from "./server/create.js";
|
|
92
78
|
|
|
93
79
|
// =============================================================================
|
|
94
|
-
//
|
|
80
|
+
// Streaming Handlers (WebSocket, SSE)
|
|
95
81
|
// =============================================================================
|
|
96
82
|
|
|
97
83
|
export {
|
|
98
|
-
//
|
|
99
|
-
createFrameworkHandler,
|
|
100
|
-
// Unified Handler (HTTP + SSE)
|
|
101
|
-
createHandler,
|
|
102
|
-
// HTTP Handler
|
|
103
|
-
createHTTPHandler,
|
|
104
|
-
createServerClientProxy,
|
|
105
|
-
// SSE Handler
|
|
84
|
+
// SSE Handler (for live queries)
|
|
106
85
|
createSSEHandler,
|
|
107
|
-
// WebSocket Handler
|
|
86
|
+
// WebSocket Handler (for live queries + subscriptions)
|
|
108
87
|
createWSHandler,
|
|
109
88
|
DEFAULT_WS_HANDLER_CONFIG,
|
|
110
|
-
type FrameworkHandlerOptions,
|
|
111
|
-
type Handler,
|
|
112
|
-
type HandlerOptions,
|
|
113
|
-
type HTTPHandler,
|
|
114
|
-
type HTTPHandlerOptions,
|
|
115
|
-
handleWebMutation,
|
|
116
|
-
handleWebQuery,
|
|
117
|
-
handleWebSSE,
|
|
118
89
|
type SSEHandlerOptions,
|
|
119
90
|
type WSHandler,
|
|
120
91
|
type WSHandlerConfig,
|
|
121
92
|
type WSHandlerOptions,
|
|
122
93
|
} from "./handlers/index.js";
|
|
123
94
|
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// Framework Integration Utilities (internal use)
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
createFrameworkHandler,
|
|
101
|
+
createServerClientProxy,
|
|
102
|
+
type FrameworkHandlerOptions,
|
|
103
|
+
handleWebMutation,
|
|
104
|
+
handleWebQuery,
|
|
105
|
+
handleWebSSE,
|
|
106
|
+
} from "./handlers/index.js";
|
|
107
|
+
|
|
124
108
|
// =============================================================================
|
|
125
109
|
// Plugin System
|
|
126
110
|
// =============================================================================
|
|
@@ -91,7 +91,7 @@ const User = model("User", {
|
|
|
91
91
|
// =============================================================================
|
|
92
92
|
|
|
93
93
|
const getUser = query()
|
|
94
|
-
.
|
|
94
|
+
.args(z.object({ id: z.string() }))
|
|
95
95
|
.returns(User)
|
|
96
96
|
.resolve(({ args }) => ({
|
|
97
97
|
id: args.id,
|
|
@@ -105,7 +105,7 @@ const getUsers = query().resolve(() => [
|
|
|
105
105
|
]);
|
|
106
106
|
|
|
107
107
|
const createUser = mutation()
|
|
108
|
-
.
|
|
108
|
+
.args(z.object({ name: z.string(), email: z.string().optional() }))
|
|
109
109
|
.returns(User)
|
|
110
110
|
.resolve(({ args }) => ({
|
|
111
111
|
id: "new-id",
|
|
@@ -114,7 +114,7 @@ const createUser = mutation()
|
|
|
114
114
|
}));
|
|
115
115
|
|
|
116
116
|
const updateUser = mutation()
|
|
117
|
-
.
|
|
117
|
+
.args(z.object({ id: z.string(), name: z.string().optional() }))
|
|
118
118
|
.returns(User)
|
|
119
119
|
.resolve(({ args }) => ({
|
|
120
120
|
id: args.id,
|
|
@@ -122,7 +122,7 @@ const updateUser = mutation()
|
|
|
122
122
|
}));
|
|
123
123
|
|
|
124
124
|
const deleteUser = mutation()
|
|
125
|
-
.
|
|
125
|
+
.args(z.object({ id: z.string() }))
|
|
126
126
|
.resolve(() => ({ success: true }));
|
|
127
127
|
|
|
128
128
|
// =============================================================================
|
|
@@ -374,7 +374,7 @@ describe("execute", () => {
|
|
|
374
374
|
|
|
375
375
|
it("handles resolver errors gracefully", async () => {
|
|
376
376
|
const errorQuery = query()
|
|
377
|
-
.
|
|
377
|
+
.args(z.object({ id: z.string() }))
|
|
378
378
|
.resolve(() => {
|
|
379
379
|
throw new Error("Resolver error");
|
|
380
380
|
});
|
|
@@ -423,7 +423,7 @@ describe("context", () => {
|
|
|
423
423
|
let capturedContext: unknown = null;
|
|
424
424
|
|
|
425
425
|
const contextQuery = query()
|
|
426
|
-
.
|
|
426
|
+
.args(z.object({ id: z.string() }))
|
|
427
427
|
.resolve(({ ctx }) => {
|
|
428
428
|
capturedContext = ctx;
|
|
429
429
|
return { id: "1", name: "test" };
|
|
@@ -451,7 +451,7 @@ describe("context", () => {
|
|
|
451
451
|
let capturedContext: unknown = null;
|
|
452
452
|
|
|
453
453
|
const contextQuery = query()
|
|
454
|
-
.
|
|
454
|
+
.args(z.object({ id: z.string() }))
|
|
455
455
|
.resolve(({ ctx }) => {
|
|
456
456
|
capturedContext = ctx;
|
|
457
457
|
return { id: "1", name: "test" };
|
|
@@ -590,7 +590,7 @@ describe("field resolvers", () => {
|
|
|
590
590
|
}));
|
|
591
591
|
|
|
592
592
|
const getAuthor = query<TestContext>()
|
|
593
|
-
.
|
|
593
|
+
.args(z.object({ id: z.string() }))
|
|
594
594
|
.returns(Author)
|
|
595
595
|
.resolve(({ args, ctx }) => {
|
|
596
596
|
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
@@ -649,7 +649,7 @@ describe("field resolvers", () => {
|
|
|
649
649
|
}));
|
|
650
650
|
|
|
651
651
|
const getAuthor = query<TestContext>()
|
|
652
|
-
.
|
|
652
|
+
.args(z.object({ id: z.string() }))
|
|
653
653
|
.returns(Author)
|
|
654
654
|
.resolve(({ args, ctx }) => {
|
|
655
655
|
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
@@ -743,7 +743,7 @@ describe("field resolvers", () => {
|
|
|
743
743
|
}));
|
|
744
744
|
|
|
745
745
|
const getAuthor = query<CtxWithComments>()
|
|
746
|
-
.
|
|
746
|
+
.args(z.object({ id: z.string() }))
|
|
747
747
|
.returns(LocalAuthor)
|
|
748
748
|
.resolve(({ args, ctx }) => {
|
|
749
749
|
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
@@ -799,7 +799,7 @@ describe("field resolvers", () => {
|
|
|
799
799
|
}));
|
|
800
800
|
|
|
801
801
|
const getAuthor = query<TestContext>()
|
|
802
|
-
.
|
|
802
|
+
.args(z.object({ id: z.string() }))
|
|
803
803
|
.returns(Author)
|
|
804
804
|
.resolve(({ args, ctx }) => {
|
|
805
805
|
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
@@ -872,7 +872,7 @@ describe("field resolvers", () => {
|
|
|
872
872
|
|
|
873
873
|
// Use .resolve().subscribe() pattern - emit comes through Publisher callback, NOT ctx
|
|
874
874
|
const getAuthor = query<{ db: typeof mockDb }>()
|
|
875
|
-
.
|
|
875
|
+
.args(z.object({ id: z.string() }))
|
|
876
876
|
.returns(Author)
|
|
877
877
|
.resolve(({ args, ctx }) => {
|
|
878
878
|
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
@@ -981,7 +981,7 @@ describe("field resolvers", () => {
|
|
|
981
981
|
}));
|
|
982
982
|
|
|
983
983
|
const getAuthor = query<{ db: typeof mockDb }>()
|
|
984
|
-
.
|
|
984
|
+
.args(z.object({ id: z.string() }))
|
|
985
985
|
.returns(Author)
|
|
986
986
|
.resolve(({ args, ctx }) => {
|
|
987
987
|
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
@@ -1070,7 +1070,7 @@ describe("field resolvers", () => {
|
|
|
1070
1070
|
}));
|
|
1071
1071
|
|
|
1072
1072
|
const getAuthor = query<{ db: typeof mockDb }>()
|
|
1073
|
-
.
|
|
1073
|
+
.args(z.object({ id: z.string() }))
|
|
1074
1074
|
.returns(Author)
|
|
1075
1075
|
.resolve(({ args, ctx }) => {
|
|
1076
1076
|
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
@@ -1146,7 +1146,7 @@ describe("field resolvers", () => {
|
|
|
1146
1146
|
|
|
1147
1147
|
// Use .resolve().subscribe() pattern - emit comes through Publisher callback
|
|
1148
1148
|
const liveQuery = query()
|
|
1149
|
-
.
|
|
1149
|
+
.args(z.object({ id: z.string() }))
|
|
1150
1150
|
.returns(User)
|
|
1151
1151
|
.resolve(({ args }) => {
|
|
1152
1152
|
return { id: args.id, name: "Initial" };
|
|
@@ -1244,7 +1244,7 @@ describe("field resolvers", () => {
|
|
|
1244
1244
|
}));
|
|
1245
1245
|
|
|
1246
1246
|
const getAuthor = query<{ db: typeof mockDb }>()
|
|
1247
|
-
.
|
|
1247
|
+
.args(z.object({ id: z.string() }))
|
|
1248
1248
|
.returns(Author)
|
|
1249
1249
|
.resolve(({ args, ctx }) => {
|
|
1250
1250
|
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
@@ -1324,7 +1324,7 @@ describe("field resolvers", () => {
|
|
|
1324
1324
|
describe("observable behavior", () => {
|
|
1325
1325
|
it("delivers initial result immediately for queries", async () => {
|
|
1326
1326
|
const simpleQuery = query()
|
|
1327
|
-
.
|
|
1327
|
+
.args(z.object({ id: z.string() }))
|
|
1328
1328
|
.resolve(({ args }) => ({ id: args.id, name: "Test" }));
|
|
1329
1329
|
|
|
1330
1330
|
const server = createApp({ queries: { simpleQuery } });
|
|
@@ -1348,7 +1348,7 @@ describe("observable behavior", () => {
|
|
|
1348
1348
|
|
|
1349
1349
|
// Use .resolve().subscribe() pattern - emit comes through Publisher callback
|
|
1350
1350
|
const liveQuery = query()
|
|
1351
|
-
.
|
|
1351
|
+
.args(z.object({ id: z.string() }))
|
|
1352
1352
|
.resolve(({ args }) => {
|
|
1353
1353
|
return { id: args.id, name: "Initial" };
|
|
1354
1354
|
})
|
|
@@ -1381,7 +1381,7 @@ describe("observable behavior", () => {
|
|
|
1381
1381
|
|
|
1382
1382
|
it("delivers mutation result via observable", async () => {
|
|
1383
1383
|
const testMutation = mutation()
|
|
1384
|
-
.
|
|
1384
|
+
.args(z.object({ name: z.string() }))
|
|
1385
1385
|
.resolve(({ args }) => ({ id: "new", name: args.name }));
|
|
1386
1386
|
|
|
1387
1387
|
const server = createApp({ mutations: { testMutation } });
|
|
@@ -1403,7 +1403,7 @@ describe("observable behavior", () => {
|
|
|
1403
1403
|
let resolverCalls = 0;
|
|
1404
1404
|
|
|
1405
1405
|
const simpleQuery = query()
|
|
1406
|
-
.
|
|
1406
|
+
.args(z.object({ id: z.string() }))
|
|
1407
1407
|
.resolve(({ args }) => {
|
|
1408
1408
|
resolverCalls++;
|
|
1409
1409
|
return { id: args.id };
|
|
@@ -1438,7 +1438,7 @@ describe("emit backpressure", () => {
|
|
|
1438
1438
|
|
|
1439
1439
|
// Use .resolve().subscribe() pattern - emit comes through Publisher callback
|
|
1440
1440
|
const liveQuery = query()
|
|
1441
|
-
.
|
|
1441
|
+
.args(z.object({ id: z.string() }))
|
|
1442
1442
|
.resolve(({ args }) => {
|
|
1443
1443
|
return { id: args.id, count: 0 };
|
|
1444
1444
|
})
|
|
@@ -1485,7 +1485,7 @@ describe("emit backpressure", () => {
|
|
|
1485
1485
|
describe("observable error handling", () => {
|
|
1486
1486
|
it("propagates resolver errors to observer", async () => {
|
|
1487
1487
|
const errorQuery = query()
|
|
1488
|
-
.
|
|
1488
|
+
.args(z.object({ id: z.string() }))
|
|
1489
1489
|
.resolve(() => {
|
|
1490
1490
|
throw new Error("Test error");
|
|
1491
1491
|
});
|
|
@@ -1507,7 +1507,7 @@ describe("observable error handling", () => {
|
|
|
1507
1507
|
|
|
1508
1508
|
it("handles async resolver errors", async () => {
|
|
1509
1509
|
const asyncErrorQuery = query()
|
|
1510
|
-
.
|
|
1510
|
+
.args(z.object({ id: z.string() }))
|
|
1511
1511
|
.resolve(async () => {
|
|
1512
1512
|
await new Promise((r) => setTimeout(r, 10));
|
|
1513
1513
|
throw new Error("Async error");
|
|
@@ -1546,7 +1546,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1546
1546
|
let capturedOnCleanup: ((fn: () => void) => void) | undefined;
|
|
1547
1547
|
|
|
1548
1548
|
const liveUser = query()
|
|
1549
|
-
.
|
|
1549
|
+
.args(z.object({ id: z.string() }))
|
|
1550
1550
|
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1551
1551
|
.subscribe(({ args: _args }) => ({ emit, onCleanup }) => {
|
|
1552
1552
|
subscriberCalled = true;
|
|
@@ -1586,7 +1586,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1586
1586
|
let capturedEmit: ((value: { id: string; name: string }) => void) | undefined;
|
|
1587
1587
|
|
|
1588
1588
|
const liveUser = query()
|
|
1589
|
-
.
|
|
1589
|
+
.args(z.object({ id: z.string() }))
|
|
1590
1590
|
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1591
1591
|
.subscribe(() => ({ emit }) => {
|
|
1592
1592
|
capturedEmit = emit;
|
|
@@ -1635,7 +1635,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1635
1635
|
let cleanupCalled = false;
|
|
1636
1636
|
|
|
1637
1637
|
const liveUser = query()
|
|
1638
|
-
.
|
|
1638
|
+
.args(z.object({ id: z.string() }))
|
|
1639
1639
|
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1640
1640
|
.subscribe(() => ({ onCleanup }) => {
|
|
1641
1641
|
onCleanup(() => {
|
|
@@ -1672,7 +1672,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1672
1672
|
let receivedCtx: TestContext | undefined;
|
|
1673
1673
|
|
|
1674
1674
|
const liveUser = query<TestContext>()
|
|
1675
|
-
.
|
|
1675
|
+
.args(z.object({ id: z.string() }))
|
|
1676
1676
|
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1677
1677
|
.subscribe(({ args, ctx }) => ({ emit: _emit }) => {
|
|
1678
1678
|
receivedInput = args;
|
|
@@ -1702,7 +1702,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1702
1702
|
|
|
1703
1703
|
it("handles subscriber errors gracefully", async () => {
|
|
1704
1704
|
const liveUser = query()
|
|
1705
|
-
.
|
|
1705
|
+
.args(z.object({ id: z.string() }))
|
|
1706
1706
|
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1707
1707
|
.subscribe(() => () => {
|
|
1708
1708
|
throw new Error("Subscriber error");
|
|
@@ -1745,7 +1745,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1745
1745
|
let capturedEmit: ((value: { id: string; count: number }) => void) | undefined;
|
|
1746
1746
|
|
|
1747
1747
|
const liveCounter = query()
|
|
1748
|
-
.
|
|
1748
|
+
.args(z.object({ id: z.string() }))
|
|
1749
1749
|
.resolve(({ args }) => ({ id: args.id, count: 0 }))
|
|
1750
1750
|
.subscribe(() => ({ emit }) => {
|
|
1751
1751
|
capturedEmit = emit;
|
|
@@ -1797,7 +1797,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1797
1797
|
let capturedEmit: EmitFn | undefined;
|
|
1798
1798
|
|
|
1799
1799
|
const liveUser = query()
|
|
1800
|
-
.
|
|
1800
|
+
.args(z.object({ id: z.string() }))
|
|
1801
1801
|
.resolve(({ args }) => ({ id: args.id, name: "Initial", status: "offline" }))
|
|
1802
1802
|
.subscribe(() => ({ emit }) => {
|
|
1803
1803
|
capturedEmit = emit as EmitFn;
|
|
@@ -1844,7 +1844,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1844
1844
|
let capturedEmit: ((value: { id: string; name: string }) => void) | undefined;
|
|
1845
1845
|
|
|
1846
1846
|
const liveUser = query()
|
|
1847
|
-
.
|
|
1847
|
+
.args(z.object({ id: z.string() }))
|
|
1848
1848
|
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1849
1849
|
.subscribe(() => ({ emit }) => {
|
|
1850
1850
|
capturedEmit = emit;
|
|
@@ -1892,7 +1892,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1892
1892
|
const emits: Array<(value: { id: string; name: string }) => void> = [];
|
|
1893
1893
|
|
|
1894
1894
|
const liveUser = query()
|
|
1895
|
-
.
|
|
1895
|
+
.args(z.object({ id: z.string() }))
|
|
1896
1896
|
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1897
1897
|
.subscribe(() => ({ emit }) => {
|
|
1898
1898
|
subscriberCallCount++;
|
|
@@ -1967,7 +1967,7 @@ describe("scalar field subscription with emit.delta()", () => {
|
|
|
1967
1967
|
entities: { UserWithBio },
|
|
1968
1968
|
queries: {
|
|
1969
1969
|
getUserWithBio: query()
|
|
1970
|
-
.
|
|
1970
|
+
.args(z.object({ id: z.string() }))
|
|
1971
1971
|
.returns(UserWithBio)
|
|
1972
1972
|
.resolve(({ args }) => ({ id: args.id, name: "Alice" })),
|
|
1973
1973
|
},
|
|
@@ -2030,7 +2030,7 @@ describe("scalar field subscription with emit.delta()", () => {
|
|
|
2030
2030
|
entities: { UserWithContent },
|
|
2031
2031
|
queries: {
|
|
2032
2032
|
getUserWithContent: query()
|
|
2033
|
-
.
|
|
2033
|
+
.args(z.object({ id: z.string() }))
|
|
2034
2034
|
.returns(UserWithContent)
|
|
2035
2035
|
.resolve(({ args }) => ({ id: args.id, name: "Alice" })),
|
|
2036
2036
|
},
|
package/src/server/create.ts
CHANGED
|
@@ -23,13 +23,16 @@ import {
|
|
|
23
23
|
createResolverFromEntity,
|
|
24
24
|
type Emit,
|
|
25
25
|
type EmitCommand,
|
|
26
|
+
firstValueFrom,
|
|
26
27
|
flattenRouter,
|
|
27
28
|
hashValue,
|
|
28
29
|
type InferRouterContext,
|
|
30
|
+
isError,
|
|
29
31
|
isLiveQueryDef,
|
|
30
32
|
isModelDef,
|
|
31
33
|
isMutationDef,
|
|
32
34
|
isQueryDef,
|
|
35
|
+
isSnapshot,
|
|
33
36
|
isSubscriptionDef,
|
|
34
37
|
type LiveQueryDef,
|
|
35
38
|
type Message,
|
|
@@ -114,8 +117,7 @@ class LensServerImpl<
|
|
|
114
117
|
M extends MutationsMap = MutationsMap,
|
|
115
118
|
S extends SubscriptionsMap = SubscriptionsMap,
|
|
116
119
|
TContext extends ContextValue = ContextValue,
|
|
117
|
-
>
|
|
118
|
-
{
|
|
120
|
+
> {
|
|
119
121
|
private queries: Q;
|
|
120
122
|
private mutations: M;
|
|
121
123
|
private subscriptions: S;
|
|
@@ -420,7 +422,7 @@ class LensServerImpl<
|
|
|
420
422
|
}
|
|
421
423
|
|
|
422
424
|
// Get the publisher function
|
|
423
|
-
const publisher = subscriber({ input, ctx: context });
|
|
425
|
+
const publisher = subscriber({ args: input, ctx: context });
|
|
424
426
|
|
|
425
427
|
// Call publisher with emit/onCleanup callbacks
|
|
426
428
|
if (publisher) {
|
|
@@ -1220,6 +1222,112 @@ class LensServerImpl<
|
|
|
1220
1222
|
getPluginManager(): PluginManager {
|
|
1221
1223
|
return this.pluginManager;
|
|
1222
1224
|
}
|
|
1225
|
+
|
|
1226
|
+
// =========================================================================
|
|
1227
|
+
// HTTP Fetch Handler
|
|
1228
|
+
// =========================================================================
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* HTTP fetch handler - Web standard Request/Response.
|
|
1232
|
+
* Works with any runtime: Bun, Deno, Cloudflare Workers, etc.
|
|
1233
|
+
*/
|
|
1234
|
+
fetch = async (request: Request): Promise<Response> => {
|
|
1235
|
+
const url = new URL(request.url);
|
|
1236
|
+
const pathname = url.pathname;
|
|
1237
|
+
|
|
1238
|
+
// Base headers including CORS and security
|
|
1239
|
+
const baseHeaders: Record<string, string> = {
|
|
1240
|
+
"Content-Type": "application/json",
|
|
1241
|
+
"X-Content-Type-Options": "nosniff",
|
|
1242
|
+
"X-Frame-Options": "DENY",
|
|
1243
|
+
"Access-Control-Allow-Origin": "*",
|
|
1244
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
1245
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
// Handle CORS preflight
|
|
1249
|
+
if (request.method === "OPTIONS") {
|
|
1250
|
+
return new Response(null, { status: 204, headers: baseHeaders });
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Health check: GET /__lens/health
|
|
1254
|
+
if (request.method === "GET" && pathname === "/__lens/health") {
|
|
1255
|
+
const metadata = this.getMetadata();
|
|
1256
|
+
return new Response(
|
|
1257
|
+
JSON.stringify({
|
|
1258
|
+
status: "healthy",
|
|
1259
|
+
service: "lens-server",
|
|
1260
|
+
version: metadata.version,
|
|
1261
|
+
timestamp: new Date().toISOString(),
|
|
1262
|
+
}),
|
|
1263
|
+
{
|
|
1264
|
+
headers: {
|
|
1265
|
+
...baseHeaders,
|
|
1266
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Metadata: GET /__lens/metadata
|
|
1273
|
+
if (request.method === "GET" && pathname === "/__lens/metadata") {
|
|
1274
|
+
return new Response(JSON.stringify(this.getMetadata()), {
|
|
1275
|
+
headers: baseHeaders,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Operations: POST /
|
|
1280
|
+
if (request.method === "POST" && (pathname === "/" || pathname === "")) {
|
|
1281
|
+
let body: { path?: string; input?: unknown };
|
|
1282
|
+
try {
|
|
1283
|
+
body = (await request.json()) as typeof body;
|
|
1284
|
+
} catch {
|
|
1285
|
+
return new Response(JSON.stringify({ error: "Invalid JSON in request body" }), {
|
|
1286
|
+
status: 400,
|
|
1287
|
+
headers: baseHeaders,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (!body.path) {
|
|
1292
|
+
return new Response(JSON.stringify({ error: "Missing operation path" }), {
|
|
1293
|
+
status: 400,
|
|
1294
|
+
headers: baseHeaders,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
try {
|
|
1299
|
+
const result = await firstValueFrom(this.execute({ path: body.path, input: body.input }));
|
|
1300
|
+
|
|
1301
|
+
if (isError(result)) {
|
|
1302
|
+
return new Response(JSON.stringify({ error: result.error }), {
|
|
1303
|
+
status: 500,
|
|
1304
|
+
headers: baseHeaders,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (isSnapshot(result)) {
|
|
1309
|
+
return new Response(JSON.stringify({ data: result.data }), {
|
|
1310
|
+
headers: baseHeaders,
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// ops message - forward as-is
|
|
1315
|
+
return new Response(JSON.stringify(result), { headers: baseHeaders });
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1318
|
+
return new Response(JSON.stringify({ error: errMsg }), {
|
|
1319
|
+
status: 500,
|
|
1320
|
+
headers: baseHeaders,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Not found
|
|
1326
|
+
return new Response(JSON.stringify({ error: "Not found" }), {
|
|
1327
|
+
status: 404,
|
|
1328
|
+
headers: baseHeaders,
|
|
1329
|
+
});
|
|
1330
|
+
};
|
|
1223
1331
|
}
|
|
1224
1332
|
|
|
1225
1333
|
// =============================================================================
|
|
@@ -1227,20 +1335,31 @@ class LensServerImpl<
|
|
|
1227
1335
|
// =============================================================================
|
|
1228
1336
|
|
|
1229
1337
|
/**
|
|
1230
|
-
* Create Lens
|
|
1338
|
+
* Create Lens app - a callable HTTP handler.
|
|
1339
|
+
*
|
|
1340
|
+
* The returned app is directly usable as a fetch handler.
|
|
1341
|
+
* Works with any runtime: Bun, Deno, Cloudflare Workers, Node.js.
|
|
1231
1342
|
*
|
|
1232
1343
|
* @example
|
|
1233
1344
|
* ```typescript
|
|
1234
|
-
* // Stateless mode (default)
|
|
1235
|
-
* const app = createApp({ router });
|
|
1236
|
-
* createWSHandler(app); // Sends full data on each update
|
|
1237
|
-
*
|
|
1238
|
-
* // Stateful mode (with clientState)
|
|
1239
1345
|
* const app = createApp({
|
|
1240
|
-
* router,
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1346
|
+
* router: appRouter,
|
|
1347
|
+
* entities: { User, Post },
|
|
1348
|
+
* resolvers: [userResolver, postResolver],
|
|
1349
|
+
* context: () => ({ db }),
|
|
1350
|
+
* })
|
|
1351
|
+
*
|
|
1352
|
+
* // Bun - app is directly callable
|
|
1353
|
+
* Bun.serve(app)
|
|
1354
|
+
*
|
|
1355
|
+
* // Deno
|
|
1356
|
+
* Deno.serve(app)
|
|
1357
|
+
*
|
|
1358
|
+
* // Cloudflare Workers
|
|
1359
|
+
* export default app
|
|
1360
|
+
*
|
|
1361
|
+
* // Or use app.fetch explicitly
|
|
1362
|
+
* Bun.serve({ fetch: app.fetch })
|
|
1244
1363
|
* ```
|
|
1245
1364
|
*/
|
|
1246
1365
|
export function createApp<
|
|
@@ -1269,7 +1388,17 @@ export function createApp<
|
|
|
1269
1388
|
config: LensServerConfig<TContext> & { queries?: Q; mutations?: M },
|
|
1270
1389
|
): LensServer & { _types: { queries: Q; mutations: M; context: TContext } } {
|
|
1271
1390
|
const server = new LensServerImpl(config) as LensServerImpl<Q, M, SubscriptionsMap, TContext>;
|
|
1272
|
-
|
|
1391
|
+
|
|
1392
|
+
// Create callable function that delegates to fetch
|
|
1393
|
+
const app = ((request: Request) => server.fetch(request)) as LensServer & {
|
|
1273
1394
|
_types: { queries: Q; mutations: M; context: TContext };
|
|
1274
1395
|
};
|
|
1396
|
+
|
|
1397
|
+
// Attach properties
|
|
1398
|
+
app.fetch = server.fetch;
|
|
1399
|
+
app.execute = server.execute.bind(server);
|
|
1400
|
+
app.getMetadata = server.getMetadata.bind(server);
|
|
1401
|
+
app.getPluginManager = server.getPluginManager.bind(server);
|
|
1402
|
+
|
|
1403
|
+
return app;
|
|
1275
1404
|
}
|