@sylphx/lens-server 2.8.1 → 2.10.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 +204 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16 -4
- package/package.json +2 -2
- package/src/server/create.ts +30 -7
- package/src/server/types.ts +2 -1
package/README.md
CHANGED
|
@@ -10,20 +10,217 @@ bun add @sylphx/lens-server
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
+
### Basic Server Setup
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createApp, createHandler } from "@sylphx/lens-server";
|
|
17
|
+
import { model, lens, router, list, nullable } from "@sylphx/lens-core";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
|
|
20
|
+
// Define context type
|
|
21
|
+
interface AppContext {
|
|
22
|
+
db: Database;
|
|
23
|
+
user: User | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Define models with inline resolvers
|
|
27
|
+
const User = model<AppContext>("User", (t) => ({
|
|
28
|
+
id: t.id(),
|
|
29
|
+
name: t.string(),
|
|
30
|
+
email: t.string(),
|
|
31
|
+
posts: t.many(() => Post).resolve(({ parent, ctx }) =>
|
|
32
|
+
ctx.db.posts.filter(p => p.authorId === parent.id)
|
|
33
|
+
),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const Post = model<AppContext>("Post", (t) => ({
|
|
37
|
+
id: t.id(),
|
|
38
|
+
title: t.string(),
|
|
39
|
+
authorId: t.string(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Create typed builders
|
|
43
|
+
const { query, mutation } = lens<AppContext>();
|
|
44
|
+
|
|
45
|
+
// Define operations
|
|
46
|
+
const appRouter = router({
|
|
47
|
+
user: {
|
|
48
|
+
get: query()
|
|
49
|
+
.input(z.object({ id: z.string() }))
|
|
50
|
+
.returns(User)
|
|
51
|
+
.resolve(({ input, ctx }) => ctx.db.users.get(input.id)!),
|
|
52
|
+
|
|
53
|
+
find: query()
|
|
54
|
+
.input(z.object({ email: z.string() }))
|
|
55
|
+
.returns(nullable(User)) // User | null
|
|
56
|
+
.resolve(({ input, ctx }) => ctx.db.users.findByEmail(input.email)),
|
|
57
|
+
|
|
58
|
+
list: query()
|
|
59
|
+
.returns(list(User)) // User[]
|
|
60
|
+
.resolve(({ ctx }) => ctx.db.users.findMany()),
|
|
61
|
+
|
|
62
|
+
update: mutation()
|
|
63
|
+
.input(z.object({ id: z.string(), name: z.string() }))
|
|
64
|
+
.returns(User)
|
|
65
|
+
.resolve(({ input, ctx }) => ctx.db.users.update(input)),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Create server - models auto-tracked from router!
|
|
70
|
+
const app = createApp({
|
|
71
|
+
router: appRouter, // Models extracted from .returns()
|
|
72
|
+
context: () => ({
|
|
73
|
+
db: database,
|
|
74
|
+
user: getCurrentUser(),
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Start server
|
|
79
|
+
const handler = createHandler(app);
|
|
80
|
+
Bun.serve({ port: 3000, fetch: handler });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### With Optimistic Updates
|
|
84
|
+
|
|
13
85
|
```typescript
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
86
|
+
import { createApp, optimisticPlugin } from "@sylphx/lens-server";
|
|
87
|
+
import { model, lens, router } from "@sylphx/lens-core";
|
|
88
|
+
import { entity as e, temp, now } from "@sylphx/reify";
|
|
16
89
|
|
|
17
|
-
|
|
90
|
+
// Enable optimistic plugin
|
|
91
|
+
const { query, mutation, plugins } = lens<AppContext>()
|
|
92
|
+
.withPlugins([optimisticPlugin()]);
|
|
93
|
+
|
|
94
|
+
const appRouter = router({
|
|
95
|
+
user: {
|
|
96
|
+
// Sugar syntax
|
|
97
|
+
update: mutation()
|
|
98
|
+
.input(z.object({ id: z.string(), name: z.string() }))
|
|
99
|
+
.returns(User)
|
|
100
|
+
.optimistic("merge") // Instant UI update
|
|
101
|
+
.resolve(({ input, ctx }) => ctx.db.users.update(input)),
|
|
102
|
+
},
|
|
103
|
+
message: {
|
|
104
|
+
// Reify DSL (multi-entity)
|
|
105
|
+
send: mutation()
|
|
106
|
+
.input(z.object({ content: z.string(), userId: z.string() }))
|
|
107
|
+
.returns(Message)
|
|
108
|
+
.optimistic(({ input }) => [
|
|
109
|
+
e.create(Message, {
|
|
110
|
+
id: temp(),
|
|
111
|
+
content: input.content,
|
|
112
|
+
createdAt: now(),
|
|
113
|
+
}),
|
|
114
|
+
])
|
|
115
|
+
.resolve(({ input, ctx }) => ctx.db.messages.create(input)),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const app = createApp({
|
|
120
|
+
router: appRouter,
|
|
121
|
+
plugins, // Include optimistic plugin
|
|
122
|
+
context: () => ({ ... }),
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Live Queries
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const { query } = lens<AppContext>();
|
|
130
|
+
|
|
131
|
+
// Live query with Publisher pattern
|
|
132
|
+
const watchUser = query()
|
|
133
|
+
.input(z.object({ id: z.string() }))
|
|
134
|
+
.resolve(({ input, ctx }) => ctx.db.users.get(input.id)!) // Initial value
|
|
135
|
+
.subscribe(({ input, ctx }) => ({ emit, onCleanup }) => {
|
|
136
|
+
// Publisher callback - emit/onCleanup passed here
|
|
137
|
+
const unsub = ctx.db.users.onChange(input.id, (user) => {
|
|
138
|
+
emit(user); // Push update to clients
|
|
139
|
+
});
|
|
140
|
+
onCleanup(unsub); // Cleanup on disconnect
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### WebSocket Handler
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { createApp, createHandler, createWSHandler } from "@sylphx/lens-server";
|
|
148
|
+
|
|
149
|
+
const app = createApp({ ... });
|
|
150
|
+
|
|
151
|
+
// HTTP handler
|
|
152
|
+
const httpHandler = createHandler(app);
|
|
153
|
+
|
|
154
|
+
// WebSocket handler
|
|
155
|
+
const wsHandler = createWSHandler(app);
|
|
18
156
|
|
|
19
|
-
// Handle WebSocket connections
|
|
20
157
|
Bun.serve({
|
|
21
158
|
port: 3000,
|
|
22
|
-
fetch
|
|
23
|
-
|
|
159
|
+
fetch(req, server) {
|
|
160
|
+
if (req.headers.get("upgrade") === "websocket") {
|
|
161
|
+
return wsHandler.upgrade(req, server);
|
|
162
|
+
}
|
|
163
|
+
return httpHandler(req);
|
|
164
|
+
},
|
|
165
|
+
websocket: wsHandler.websocket,
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## createApp Options
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
createApp({
|
|
173
|
+
// Required (at least one)
|
|
174
|
+
router: RouterDef, // Namespaced operations
|
|
175
|
+
|
|
176
|
+
// Optional (models auto-tracked from router!)
|
|
177
|
+
entities: EntitiesMap, // Explicit models (optional, for overrides)
|
|
178
|
+
plugins: ServerPlugin[], // Server plugins (optimistic, clientState, etc.)
|
|
179
|
+
context: () => TContext, // Context factory
|
|
180
|
+
logger: LensLogger, // Logging (default: silent)
|
|
181
|
+
version: string, // Server version (default: "1.0.0")
|
|
24
182
|
});
|
|
25
183
|
```
|
|
26
184
|
|
|
185
|
+
## Auto-tracking Models
|
|
186
|
+
|
|
187
|
+
Models are automatically collected from router return types:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// These models are auto-tracked:
|
|
191
|
+
const appRouter = router({
|
|
192
|
+
user: {
|
|
193
|
+
get: query().returns(User).resolve(...), // User tracked
|
|
194
|
+
list: query().returns(list(User)).resolve(...), // User tracked
|
|
195
|
+
find: query().returns(nullable(User)).resolve(...), // User tracked
|
|
196
|
+
},
|
|
197
|
+
post: {
|
|
198
|
+
get: query().returns(Post).resolve(...), // Post tracked
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// No need to pass entities explicitly
|
|
203
|
+
const app = createApp({
|
|
204
|
+
router: appRouter, // User and Post auto-collected
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Or override/add explicit models
|
|
208
|
+
const app = createApp({
|
|
209
|
+
router: appRouter,
|
|
210
|
+
entities: { User, Post, ExtraModel }, // Explicit takes priority
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Optimistic Update Strategies
|
|
215
|
+
|
|
216
|
+
| Strategy | Description | Example |
|
|
217
|
+
|----------|-------------|---------|
|
|
218
|
+
| `"merge"` | Merge input into entity | `.optimistic("merge")` |
|
|
219
|
+
| `"create"` | Create with temp ID | `.optimistic("create")` |
|
|
220
|
+
| `"delete"` | Mark entity deleted | `.optimistic("delete")` |
|
|
221
|
+
| `{ merge: {...} }` | Merge with extra fields | `.optimistic({ merge: { status: "pending" } })` |
|
|
222
|
+
| Reify DSL | Multi-entity operations | `.optimistic(({ input }) => [...])` |
|
|
223
|
+
|
|
27
224
|
## License
|
|
28
225
|
|
|
29
226
|
MIT
|
|
@@ -32,4 +229,4 @@ MIT
|
|
|
32
229
|
|
|
33
230
|
Built with [@sylphx/lens-core](https://github.com/SylphxAI/Lens).
|
|
34
231
|
|
|
35
|
-
|
|
232
|
+
Powered by Sylphx
|
package/dist/index.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ declare function extendContext<
|
|
|
41
41
|
E extends ContextValue
|
|
42
42
|
>(current: T, extension: E): T & E;
|
|
43
43
|
import { ContextValue as ContextValue3, InferRouterContext as InferRouterContext2, RouterDef as RouterDef2 } from "@sylphx/lens-core";
|
|
44
|
-
import { ContextValue as ContextValue2, EntityDef, InferRouterContext, MutationDef, Observable, OptimisticDSL, QueryDef, Resolvers, RouterDef } from "@sylphx/lens-core";
|
|
44
|
+
import { AnyQueryDef, ContextValue as ContextValue2, EntityDef, InferRouterContext, MutationDef, Observable, OptimisticDSL, QueryDef, Resolvers, RouterDef } from "@sylphx/lens-core";
|
|
45
45
|
/**
|
|
46
46
|
* @sylphx/lens-server - Plugin System Types
|
|
47
47
|
*
|
|
@@ -461,7 +461,7 @@ interface SelectionObject {
|
|
|
461
461
|
/** Entity map type */
|
|
462
462
|
type EntitiesMap = Record<string, EntityDef<string, any>>;
|
|
463
463
|
/** Queries map type */
|
|
464
|
-
type QueriesMap = Record<string,
|
|
464
|
+
type QueriesMap = Record<string, AnyQueryDef<unknown, unknown>>;
|
|
465
465
|
/** Mutations map type */
|
|
466
466
|
type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
|
|
467
467
|
/** Operation metadata for handshake */
|
package/dist/index.js
CHANGED
|
@@ -35,14 +35,18 @@ function extendContext(current, extension) {
|
|
|
35
35
|
}
|
|
36
36
|
// src/server/create.ts
|
|
37
37
|
import {
|
|
38
|
+
collectModelsFromOperations,
|
|
39
|
+
collectModelsFromRouter,
|
|
38
40
|
createEmit,
|
|
39
41
|
createResolverFromEntity,
|
|
40
42
|
flattenRouter,
|
|
41
43
|
hashValue,
|
|
42
44
|
hasInlineResolvers,
|
|
43
45
|
isEntityDef,
|
|
46
|
+
isModelDef,
|
|
44
47
|
isMutationDef,
|
|
45
48
|
isQueryDef,
|
|
49
|
+
mergeModelCollections,
|
|
46
50
|
valuesEqual
|
|
47
51
|
} from "@sylphx/lens-core";
|
|
48
52
|
|
|
@@ -354,15 +358,23 @@ class LensServerImpl {
|
|
|
354
358
|
}
|
|
355
359
|
this.queries = queries;
|
|
356
360
|
this.mutations = mutations;
|
|
357
|
-
const
|
|
361
|
+
const autoCollected = config.router ? collectModelsFromRouter(config.router) : collectModelsFromOperations(queries, mutations);
|
|
362
|
+
const entitiesFromConfig = config.entities ?? {};
|
|
363
|
+
const mergedModels = mergeModelCollections(autoCollected, entitiesFromConfig);
|
|
358
364
|
if (config.resolvers) {
|
|
359
365
|
for (const resolver of config.resolvers) {
|
|
360
366
|
const entityName = resolver.entity._name;
|
|
361
|
-
if (entityName && !
|
|
362
|
-
|
|
367
|
+
if (entityName && !mergedModels.has(entityName)) {
|
|
368
|
+
mergedModels.set(entityName, resolver.entity);
|
|
363
369
|
}
|
|
364
370
|
}
|
|
365
371
|
}
|
|
372
|
+
const entities = {};
|
|
373
|
+
for (const [name, model] of mergedModels) {
|
|
374
|
+
if (isEntityDef(model) || isModelDef(model)) {
|
|
375
|
+
entities[name] = model;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
366
378
|
this.entities = entities;
|
|
367
379
|
this.resolverMap = this.buildResolverMap(config.resolvers, entities);
|
|
368
380
|
this.contextFactory = config.context ?? (() => ({}));
|
|
@@ -415,7 +427,7 @@ class LensServerImpl {
|
|
|
415
427
|
}
|
|
416
428
|
}
|
|
417
429
|
for (const [name, entity] of Object.entries(entities)) {
|
|
418
|
-
if (!isEntityDef(entity))
|
|
430
|
+
if (!isEntityDef(entity) && !isModelDef(entity))
|
|
419
431
|
continue;
|
|
420
432
|
if (resolverMap.has(name))
|
|
421
433
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/lens-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
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": "^2.
|
|
33
|
+
"@sylphx/lens-core": "^2.8.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.9.3",
|
package/src/server/create.ts
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
type ContextValue,
|
|
20
|
+
collectModelsFromOperations,
|
|
21
|
+
collectModelsFromRouter,
|
|
20
22
|
createEmit,
|
|
21
23
|
createResolverFromEntity,
|
|
22
24
|
type Emit,
|
|
@@ -27,8 +29,10 @@ import {
|
|
|
27
29
|
hasInlineResolvers,
|
|
28
30
|
type InferRouterContext,
|
|
29
31
|
isEntityDef,
|
|
32
|
+
isModelDef,
|
|
30
33
|
isMutationDef,
|
|
31
34
|
isQueryDef,
|
|
35
|
+
mergeModelCollections,
|
|
32
36
|
type Observable,
|
|
33
37
|
type ResolverDef,
|
|
34
38
|
type RouterDef,
|
|
@@ -192,16 +196,34 @@ class LensServerImpl<
|
|
|
192
196
|
this.queries = queries as Q;
|
|
193
197
|
this.mutations = mutations as M;
|
|
194
198
|
|
|
195
|
-
// Build entities map: explicit config
|
|
196
|
-
|
|
199
|
+
// Build entities map (priority: explicit config > router > resolvers)
|
|
200
|
+
// Auto-track models from router return types (new behavior)
|
|
201
|
+
const autoCollected = config.router
|
|
202
|
+
? collectModelsFromRouter(config.router)
|
|
203
|
+
: collectModelsFromOperations(queries, mutations);
|
|
204
|
+
|
|
205
|
+
// Merge: explicit entities override auto-collected
|
|
206
|
+
const entitiesFromConfig = config.entities ?? {};
|
|
207
|
+
const mergedModels = mergeModelCollections(autoCollected, entitiesFromConfig);
|
|
208
|
+
|
|
209
|
+
// Also extract from explicit resolvers (legacy)
|
|
197
210
|
if (config.resolvers) {
|
|
198
211
|
for (const resolver of config.resolvers) {
|
|
199
212
|
const entityName = resolver.entity._name;
|
|
200
|
-
if (entityName && !
|
|
201
|
-
|
|
213
|
+
if (entityName && !mergedModels.has(entityName)) {
|
|
214
|
+
mergedModels.set(entityName, resolver.entity);
|
|
202
215
|
}
|
|
203
216
|
}
|
|
204
217
|
}
|
|
218
|
+
|
|
219
|
+
// Convert Map to Record for entities (supports both EntityDef and ModelDef)
|
|
220
|
+
const entities: EntitiesMap = {};
|
|
221
|
+
for (const [name, model] of mergedModels) {
|
|
222
|
+
// Both ModelDef and EntityDef work as EntitiesMap values
|
|
223
|
+
if (isEntityDef(model) || isModelDef(model)) {
|
|
224
|
+
entities[name] = model as EntityDef<string, any>;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
205
227
|
this.entities = entities;
|
|
206
228
|
|
|
207
229
|
// Build resolver map: explicit resolvers + auto-converted from entities with inline resolvers
|
|
@@ -278,12 +300,13 @@ class LensServerImpl<
|
|
|
278
300
|
}
|
|
279
301
|
}
|
|
280
302
|
|
|
281
|
-
// 2. Auto-convert entities with inline resolvers (if not already in map)
|
|
303
|
+
// 2. Auto-convert entities/models with inline resolvers (if not already in map)
|
|
282
304
|
for (const [name, entity] of Object.entries(entities)) {
|
|
283
|
-
|
|
305
|
+
// Support both EntityDef and ModelDef
|
|
306
|
+
if (!isEntityDef(entity) && !isModelDef(entity)) continue;
|
|
284
307
|
if (resolverMap.has(name)) continue; // Explicit resolver takes priority
|
|
285
308
|
|
|
286
|
-
// Check if entity has inline resolvers
|
|
309
|
+
// Check if entity/model has inline resolvers
|
|
287
310
|
if (hasInlineResolvers(entity)) {
|
|
288
311
|
const resolver = createResolverFromEntity(entity);
|
|
289
312
|
resolverMap.set(name, resolver);
|
package/src/server/types.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
+
AnyQueryDef,
|
|
8
9
|
ContextValue,
|
|
9
10
|
EntityDef,
|
|
10
11
|
InferRouterContext,
|
|
@@ -51,7 +52,7 @@ export interface SelectionObject {
|
|
|
51
52
|
export type EntitiesMap = Record<string, EntityDef<string, any>>;
|
|
52
53
|
|
|
53
54
|
/** Queries map type */
|
|
54
|
-
export type QueriesMap = Record<string,
|
|
55
|
+
export type QueriesMap = Record<string, AnyQueryDef<unknown, unknown>>;
|
|
55
56
|
|
|
56
57
|
/** Mutations map type */
|
|
57
58
|
export type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
|