@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sylphx/lens-server
2
2
 
3
- Server runtime for the Lens API framework with WebSocket support.
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
- ## Usage
12
-
13
- ### Basic Server Setup
11
+ ## Quick Start
14
12
 
15
13
  ```typescript
16
- import { createApp, createHandler } from "@sylphx/lens-server";
17
- import { lens, id, string, list, nullable, router } from "@sylphx/lens-core";
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 context type
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
- const Post = model("Post", {
41
- id: id(),
42
- title: string(),
43
- authorId: string(),
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
- .input(z.object({ id: z.string() }))
39
+ .args(z.object({ id: z.string() }))
51
40
  .returns(User)
52
- .resolve(({ input, ctx }) => ctx.db.users.get(input.id)!),
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)) // 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 server - models auto-tracked from router!
49
+ // Create app
71
50
  const app = createApp({
72
- router: appRouter, // Models extracted from .returns()
73
- context: () => ({
74
- db: database,
75
- user: getCurrentUser(),
76
- }),
51
+ router: appRouter,
52
+ entities: { User },
53
+ resolvers: [userResolver],
54
+ context: () => ({ db: database }),
77
55
  });
78
56
 
79
- // Start server
80
- const handler = createHandler(app);
81
- Bun.serve({ port: 3000, fetch: handler });
57
+ // Start server - app is directly callable
58
+ Bun.serve({ fetch: app });
82
59
  ```
83
60
 
84
- ### With Optimistic Updates
61
+ ## Runtime Support
85
62
 
86
- ```typescript
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
- // Enable optimistic plugin
92
- const { model, query, mutation, plugins } = lens<AppContext>()
93
- .withPlugins([optimisticPlugin()]);
65
+ ```typescript
66
+ // Bun
67
+ Bun.serve({ fetch: app })
94
68
 
95
- const Message = model("Message", {
96
- id: id(),
97
- content: string(),
98
- createdAt: string(),
99
- });
69
+ // Deno
70
+ Deno.serve(app)
100
71
 
101
- const appRouter = router({
102
- user: {
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
- const app = createApp({
127
- router: appRouter,
128
- plugins, // Include optimistic plugin
129
- context: () => ({ ... }),
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
- ### Live Queries
81
+ ## HTTP Endpoints
134
82
 
135
- ```typescript
136
- // query comes from lens<AppContext>() above
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
- // Live query with Publisher pattern
91
+ ## Live Queries
92
+
93
+ For real-time updates, use the Publisher pattern:
94
+
95
+ ```typescript
139
96
  const watchUser = query()
140
- .input(z.object({ id: z.string() }))
141
- .resolve(({ input, ctx }) => ctx.db.users.get(input.id)!) // Initial value
142
- .subscribe(({ input, ctx }) => ({ emit, onCleanup }) => {
143
- // Publisher callback - emit/onCleanup passed here
144
- const unsub = ctx.db.users.onChange(input.id, (user) => {
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); // Cleanup on disconnect
103
+ onCleanup(unsub);
148
104
  });
149
105
  ```
150
106
 
151
- ### WebSocket Handler
107
+ ## Optimistic Updates
152
108
 
153
109
  ```typescript
154
- import { createApp, createHandler, createWSHandler } from "@sylphx/lens-server";
155
-
156
- const app = createApp({ ... });
110
+ import { optimisticPlugin } from "@sylphx/lens-server";
157
111
 
158
- // HTTP handler
159
- const httpHandler = createHandler(app);
112
+ const { mutation, plugins } = lens<AppContext>()
113
+ .withPlugins([optimisticPlugin()]);
160
114
 
161
- // WebSocket handler
162
- const wsHandler = createWSHandler(app);
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
- Bun.serve({
165
- port: 3000,
166
- fetch(req, server) {
167
- if (req.headers.get("upgrade") === "websocket") {
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 (at least one)
132
+ // Required
181
133
  router: RouterDef, // Namespaced operations
182
134
 
183
- // Optional (models auto-tracked from router!)
184
- entities: EntitiesMap, // Explicit models (optional, for overrides)
185
- plugins: ServerPlugin[], // Server plugins (optimistic, clientState, etc.)
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