@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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @sylphx/lens-server
|
|
2
2
|
|
|
3
|
-
Server runtime for the Lens API framework
|
|
3
|
+
Server runtime for the Lens API framework.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,168 +8,120 @@ Server runtime for the Lens API framework with WebSocket support.
|
|
|
8
8
|
bun add @sylphx/lens-server
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
### Basic Server Setup
|
|
11
|
+
## Quick Start
|
|
14
12
|
|
|
15
13
|
```typescript
|
|
16
|
-
import { createApp
|
|
17
|
-
import {
|
|
14
|
+
import { createApp } from "@sylphx/lens-server";
|
|
15
|
+
import { model, id, string, list, router, lens } from "@sylphx/lens-core";
|
|
18
16
|
import { z } from "zod";
|
|
19
17
|
|
|
20
|
-
// Define
|
|
21
|
-
interface AppContext {
|
|
22
|
-
db: Database;
|
|
23
|
-
user: User | null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Create typed builders
|
|
27
|
-
const { model, query, mutation } = lens<AppContext>();
|
|
28
|
-
|
|
29
|
-
// Define models with inline resolvers
|
|
18
|
+
// Define models
|
|
30
19
|
const User = model("User", {
|
|
31
20
|
id: id(),
|
|
32
21
|
name: string(),
|
|
33
22
|
email: string(),
|
|
34
|
-
posts: list(() => Post),
|
|
35
|
-
}).resolve({
|
|
36
|
-
posts: ({ source, ctx }) =>
|
|
37
|
-
ctx.db.posts.filter(p => p.authorId === source.id)
|
|
38
23
|
});
|
|
39
24
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
// Create typed builders
|
|
26
|
+
const { query, mutation, resolver } = lens<{ db: Database }>();
|
|
27
|
+
|
|
28
|
+
// Define resolver
|
|
29
|
+
const userResolver = resolver(User, (t) => ({
|
|
30
|
+
id: t.expose('id'),
|
|
31
|
+
name: t.expose('name'),
|
|
32
|
+
email: t.expose('email'),
|
|
33
|
+
}));
|
|
45
34
|
|
|
46
35
|
// Define operations
|
|
47
36
|
const appRouter = router({
|
|
48
37
|
user: {
|
|
49
38
|
get: query()
|
|
50
|
-
.
|
|
39
|
+
.args(z.object({ id: z.string() }))
|
|
51
40
|
.returns(User)
|
|
52
|
-
.resolve(({
|
|
53
|
-
|
|
54
|
-
find: query()
|
|
55
|
-
.input(z.object({ email: z.string() }))
|
|
56
|
-
.returns(nullable(User)) // User | null
|
|
57
|
-
.resolve(({ input, ctx }) => ctx.db.users.findByEmail(input.email)),
|
|
41
|
+
.resolve(({ args, ctx }) => ctx.db.users.get(args.id)!),
|
|
58
42
|
|
|
59
43
|
list: query()
|
|
60
|
-
.returns(list(User))
|
|
44
|
+
.returns(list(User))
|
|
61
45
|
.resolve(({ ctx }) => ctx.db.users.findMany()),
|
|
62
|
-
|
|
63
|
-
update: mutation()
|
|
64
|
-
.input(z.object({ id: z.string(), name: z.string() }))
|
|
65
|
-
.returns(User)
|
|
66
|
-
.resolve(({ input, ctx }) => ctx.db.users.update(input)),
|
|
67
46
|
},
|
|
68
47
|
});
|
|
69
48
|
|
|
70
|
-
// Create
|
|
49
|
+
// Create app
|
|
71
50
|
const app = createApp({
|
|
72
|
-
router: appRouter,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}),
|
|
51
|
+
router: appRouter,
|
|
52
|
+
entities: { User },
|
|
53
|
+
resolvers: [userResolver],
|
|
54
|
+
context: () => ({ db: database }),
|
|
77
55
|
});
|
|
78
56
|
|
|
79
|
-
// Start server
|
|
80
|
-
|
|
81
|
-
Bun.serve({ port: 3000, fetch: handler });
|
|
57
|
+
// Start server - app is directly callable
|
|
58
|
+
Bun.serve({ fetch: app });
|
|
82
59
|
```
|
|
83
60
|
|
|
84
|
-
|
|
61
|
+
## Runtime Support
|
|
85
62
|
|
|
86
|
-
|
|
87
|
-
import { createApp, optimisticPlugin } from "@sylphx/lens-server";
|
|
88
|
-
import { lens, id, string, router } from "@sylphx/lens-core";
|
|
89
|
-
import { entity as e, temp, now } from "@sylphx/reify";
|
|
63
|
+
Works with any runtime - app is directly callable:
|
|
90
64
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
65
|
+
```typescript
|
|
66
|
+
// Bun
|
|
67
|
+
Bun.serve({ fetch: app })
|
|
94
68
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
content: string(),
|
|
98
|
-
createdAt: string(),
|
|
99
|
-
});
|
|
69
|
+
// Deno
|
|
70
|
+
Deno.serve(app)
|
|
100
71
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Sugar syntax
|
|
104
|
-
update: mutation()
|
|
105
|
-
.input(z.object({ id: z.string(), name: z.string() }))
|
|
106
|
-
.returns(User)
|
|
107
|
-
.optimistic("merge") // Instant UI update
|
|
108
|
-
.resolve(({ input, ctx }) => ctx.db.users.update(input)),
|
|
109
|
-
},
|
|
110
|
-
message: {
|
|
111
|
-
// Reify DSL (multi-entity)
|
|
112
|
-
send: mutation()
|
|
113
|
-
.input(z.object({ content: z.string(), userId: z.string() }))
|
|
114
|
-
.returns(Message)
|
|
115
|
-
.optimistic(({ input }) => [
|
|
116
|
-
e.create(Message, {
|
|
117
|
-
id: temp(),
|
|
118
|
-
content: input.content,
|
|
119
|
-
createdAt: now(),
|
|
120
|
-
}),
|
|
121
|
-
])
|
|
122
|
-
.resolve(({ input, ctx }) => ctx.db.messages.create(input)),
|
|
123
|
-
},
|
|
124
|
-
});
|
|
72
|
+
// Cloudflare Workers
|
|
73
|
+
export default app
|
|
125
74
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
75
|
+
// Node.js (with adapter)
|
|
76
|
+
import { createServer } from "node:http";
|
|
77
|
+
import { toNodeHandler } from "@whatwg-node/server";
|
|
78
|
+
createServer(toNodeHandler(app)).listen(3000);
|
|
131
79
|
```
|
|
132
80
|
|
|
133
|
-
|
|
81
|
+
## HTTP Endpoints
|
|
134
82
|
|
|
135
|
-
|
|
136
|
-
|
|
83
|
+
The `app.fetch` handler provides:
|
|
84
|
+
|
|
85
|
+
| Endpoint | Method | Description |
|
|
86
|
+
|----------|--------|-------------|
|
|
87
|
+
| `/` | POST | Execute operations |
|
|
88
|
+
| `/__lens/metadata` | GET | Server metadata |
|
|
89
|
+
| `/__lens/health` | GET | Health check |
|
|
137
90
|
|
|
138
|
-
|
|
91
|
+
## Live Queries
|
|
92
|
+
|
|
93
|
+
For real-time updates, use the Publisher pattern:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
139
96
|
const watchUser = query()
|
|
140
|
-
.
|
|
141
|
-
.resolve(({
|
|
142
|
-
.subscribe(({
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
emit(user); // Push update to clients
|
|
97
|
+
.args(z.object({ id: z.string() }))
|
|
98
|
+
.resolve(({ args, ctx }) => ctx.db.users.get(args.id)!)
|
|
99
|
+
.subscribe(({ args, ctx }) => ({ emit, onCleanup }) => {
|
|
100
|
+
const unsub = ctx.db.users.onChange(args.id, (user) => {
|
|
101
|
+
emit(user);
|
|
146
102
|
});
|
|
147
|
-
onCleanup(unsub);
|
|
103
|
+
onCleanup(unsub);
|
|
148
104
|
});
|
|
149
105
|
```
|
|
150
106
|
|
|
151
|
-
|
|
107
|
+
## Optimistic Updates
|
|
152
108
|
|
|
153
109
|
```typescript
|
|
154
|
-
import {
|
|
155
|
-
|
|
156
|
-
const app = createApp({ ... });
|
|
110
|
+
import { optimisticPlugin } from "@sylphx/lens-server";
|
|
157
111
|
|
|
158
|
-
|
|
159
|
-
|
|
112
|
+
const { mutation, plugins } = lens<AppContext>()
|
|
113
|
+
.withPlugins([optimisticPlugin()]);
|
|
160
114
|
|
|
161
|
-
|
|
162
|
-
|
|
115
|
+
const updateUser = mutation()
|
|
116
|
+
.args(z.object({ id: z.string(), name: z.string() }))
|
|
117
|
+
.returns(User)
|
|
118
|
+
.optimistic("merge")
|
|
119
|
+
.resolve(({ args, ctx }) => ctx.db.users.update(args));
|
|
163
120
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return wsHandler.upgrade(req, server);
|
|
169
|
-
}
|
|
170
|
-
return httpHandler(req);
|
|
171
|
-
},
|
|
172
|
-
websocket: wsHandler.websocket,
|
|
121
|
+
const app = createApp({
|
|
122
|
+
router: appRouter,
|
|
123
|
+
plugins,
|
|
124
|
+
// ...
|
|
173
125
|
});
|
|
174
126
|
```
|
|
175
127
|
|
|
@@ -177,57 +129,21 @@ Bun.serve({
|
|
|
177
129
|
|
|
178
130
|
```typescript
|
|
179
131
|
createApp({
|
|
180
|
-
// Required
|
|
132
|
+
// Required
|
|
181
133
|
router: RouterDef, // Namespaced operations
|
|
182
134
|
|
|
183
|
-
//
|
|
184
|
-
entities: EntitiesMap, //
|
|
185
|
-
|
|
135
|
+
// Entities & Resolvers
|
|
136
|
+
entities: EntitiesMap, // Models for normalization
|
|
137
|
+
resolvers: ResolverDef[], // Field resolvers array
|
|
138
|
+
|
|
139
|
+
// Optional
|
|
140
|
+
plugins: ServerPlugin[], // Server plugins (optimistic, etc.)
|
|
186
141
|
context: () => TContext, // Context factory
|
|
187
142
|
logger: LensLogger, // Logging (default: silent)
|
|
188
143
|
version: string, // Server version (default: "1.0.0")
|
|
189
144
|
});
|
|
190
145
|
```
|
|
191
146
|
|
|
192
|
-
## Auto-tracking Models
|
|
193
|
-
|
|
194
|
-
Models are automatically collected from router return types:
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
// These models are auto-tracked:
|
|
198
|
-
const appRouter = router({
|
|
199
|
-
user: {
|
|
200
|
-
get: query().returns(User).resolve(...), // User tracked
|
|
201
|
-
list: query().returns(list(User)).resolve(...), // User tracked
|
|
202
|
-
find: query().returns(nullable(User)).resolve(...), // User tracked
|
|
203
|
-
},
|
|
204
|
-
post: {
|
|
205
|
-
get: query().returns(Post).resolve(...), // Post tracked
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// No need to pass entities explicitly
|
|
210
|
-
const app = createApp({
|
|
211
|
-
router: appRouter, // User and Post auto-collected
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// Or override/add explicit models
|
|
215
|
-
const app = createApp({
|
|
216
|
-
router: appRouter,
|
|
217
|
-
entities: { User, Post, ExtraModel }, // Explicit takes priority
|
|
218
|
-
});
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
## Optimistic Update Strategies
|
|
222
|
-
|
|
223
|
-
| Strategy | Description | Example |
|
|
224
|
-
|----------|-------------|---------|
|
|
225
|
-
| `"merge"` | Merge input into entity | `.optimistic("merge")` |
|
|
226
|
-
| `"create"` | Create with temp ID | `.optimistic("create")` |
|
|
227
|
-
| `"delete"` | Mark entity deleted | `.optimistic("delete")` |
|
|
228
|
-
| `{ merge: {...} }` | Merge with extra fields | `.optimistic({ merge: { status: "pending" } })` |
|
|
229
|
-
| Reify DSL | Multi-entity operations | `.optimistic(({ input }) => [...])` |
|
|
230
|
-
|
|
231
147
|
## License
|
|
232
148
|
|
|
233
149
|
MIT
|