@sylphx/lens-server 1.3.2 → 1.5.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 +35 -0
- package/dist/index.d.ts +23 -109
- package/dist/index.js +58 -38
- package/package.json +37 -36
- package/src/e2e/server.test.ts +56 -45
- package/src/index.ts +26 -29
- package/src/server/create.test.ts +997 -20
- package/src/server/create.ts +82 -85
- package/src/sse/handler.ts +1 -1
- package/src/state/graph-state-manager.test.ts +566 -10
- package/src/state/graph-state-manager.ts +38 -13
- package/src/state/index.ts +3 -3
package/src/server/create.ts
CHANGED
|
@@ -10,27 +10,24 @@
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
type ContextValue,
|
|
13
|
+
createContext,
|
|
14
|
+
createEmit,
|
|
15
|
+
createUpdate,
|
|
13
16
|
type EmitCommand,
|
|
14
17
|
type EntityDef,
|
|
15
|
-
type EntityDefinition,
|
|
16
|
-
type EntityResolvers,
|
|
17
|
-
type EntityResolversDefinition,
|
|
18
18
|
type FieldType,
|
|
19
|
+
flattenRouter,
|
|
19
20
|
type InferRouterContext,
|
|
21
|
+
isMutationDef,
|
|
22
|
+
isQueryDef,
|
|
20
23
|
type MutationDef,
|
|
21
24
|
type QueryDef,
|
|
22
|
-
type
|
|
23
|
-
type
|
|
25
|
+
type ResolverDef,
|
|
26
|
+
type Resolvers,
|
|
24
27
|
type RouterDef,
|
|
25
|
-
type Update,
|
|
26
|
-
createContext,
|
|
27
|
-
createEmit,
|
|
28
|
-
createUpdate,
|
|
29
|
-
flattenRouter,
|
|
30
|
-
isBatchResolver,
|
|
31
|
-
isMutationDef,
|
|
32
|
-
isQueryDef,
|
|
33
28
|
runWithContext,
|
|
29
|
+
toResolverMap,
|
|
30
|
+
type Update,
|
|
34
31
|
} from "@sylphx/lens-core";
|
|
35
32
|
|
|
36
33
|
/** Selection object type for nested field selection */
|
|
@@ -45,7 +42,6 @@ import { GraphStateManager } from "../state/graph-state-manager";
|
|
|
45
42
|
// =============================================================================
|
|
46
43
|
|
|
47
44
|
/** Entity map type */
|
|
48
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
45
|
export type EntitiesMap = Record<string, EntityDef<string, any>>;
|
|
50
46
|
|
|
51
47
|
/** Queries map type */
|
|
@@ -54,15 +50,12 @@ export type QueriesMap = Record<string, QueryDef<unknown, unknown>>;
|
|
|
54
50
|
/** Mutations map type */
|
|
55
51
|
export type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
|
|
56
52
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
EntityDef<string, EntityDefinition>,
|
|
60
|
-
Record<string, RelationTypeWithForeignKey>
|
|
61
|
-
>[];
|
|
53
|
+
/** Resolver map type for internal use (uses any to avoid complex variance issues) */
|
|
54
|
+
type ResolverMap = Map<string, ResolverDef<any, any, any>>;
|
|
62
55
|
|
|
63
56
|
/** Operation metadata for handshake */
|
|
64
57
|
export interface OperationMeta {
|
|
65
|
-
type: "query" | "mutation";
|
|
58
|
+
type: "query" | "mutation" | "subscription";
|
|
66
59
|
optimistic?: unknown; // OptimisticDSL - sent as JSON
|
|
67
60
|
}
|
|
68
61
|
|
|
@@ -71,6 +64,13 @@ export type OperationsMap = {
|
|
|
71
64
|
[key: string]: OperationMeta | OperationsMap;
|
|
72
65
|
};
|
|
73
66
|
|
|
67
|
+
/** Logger interface for server */
|
|
68
|
+
export interface LensLogger {
|
|
69
|
+
info?: (message: string, ...args: unknown[]) => void;
|
|
70
|
+
warn?: (message: string, ...args: unknown[]) => void;
|
|
71
|
+
error?: (message: string, ...args: unknown[]) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
74
|
/** Server configuration */
|
|
75
75
|
export interface LensServerConfig<
|
|
76
76
|
TContext extends ContextValue = ContextValue,
|
|
@@ -78,16 +78,16 @@ export interface LensServerConfig<
|
|
|
78
78
|
> {
|
|
79
79
|
/** Entity definitions */
|
|
80
80
|
entities?: EntitiesMap;
|
|
81
|
-
/** Relation definitions */
|
|
82
|
-
relations?: RelationsArray;
|
|
83
81
|
/** Router definition (namespaced operations) - context type is inferred */
|
|
84
82
|
router?: TRouter;
|
|
85
83
|
/** Query definitions (flat, legacy) */
|
|
86
84
|
queries?: QueriesMap;
|
|
87
85
|
/** Mutation definitions (flat, legacy) */
|
|
88
86
|
mutations?: MutationsMap;
|
|
89
|
-
/**
|
|
90
|
-
resolvers?:
|
|
87
|
+
/** Field resolvers array (use lens() factory to create) */
|
|
88
|
+
resolvers?: Resolvers;
|
|
89
|
+
/** Logger for server messages (default: silent) */
|
|
90
|
+
logger?: LensLogger;
|
|
91
91
|
/** Context factory - must return the context type expected by the router */
|
|
92
92
|
context?: (req?: unknown) => TContext | Promise<TContext>;
|
|
93
93
|
/** Server version */
|
|
@@ -149,12 +149,6 @@ export interface WebSocketLike {
|
|
|
149
149
|
onerror?: ((error: unknown) => void) | null;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
/** Emit context for streaming resolvers */
|
|
153
|
-
interface EmitContext<T> {
|
|
154
|
-
emit: (data: T) => void;
|
|
155
|
-
onCleanup: (fn: () => void) => () => void;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
152
|
// =============================================================================
|
|
159
153
|
// Protocol Messages
|
|
160
154
|
// =============================================================================
|
|
@@ -285,11 +279,11 @@ class DataLoader<K, V> {
|
|
|
285
279
|
keys.forEach((key, index) => {
|
|
286
280
|
const callbacks = batch.get(key)!;
|
|
287
281
|
const result = results[index] ?? null;
|
|
288
|
-
|
|
282
|
+
for (const { resolve } of callbacks) resolve(result);
|
|
289
283
|
});
|
|
290
284
|
} catch (error) {
|
|
291
285
|
for (const callbacks of batch.values()) {
|
|
292
|
-
|
|
286
|
+
for (const { reject } of callbacks) reject(error as Error);
|
|
293
287
|
}
|
|
294
288
|
}
|
|
295
289
|
}
|
|
@@ -303,6 +297,9 @@ class DataLoader<K, V> {
|
|
|
303
297
|
// Lens Server Implementation
|
|
304
298
|
// =============================================================================
|
|
305
299
|
|
|
300
|
+
/** No-op logger (default - silent) */
|
|
301
|
+
const noopLogger: LensLogger = {};
|
|
302
|
+
|
|
306
303
|
class LensServerImpl<
|
|
307
304
|
Q extends QueriesMap = QueriesMap,
|
|
308
305
|
M extends MutationsMap = MutationsMap,
|
|
@@ -312,9 +309,10 @@ class LensServerImpl<
|
|
|
312
309
|
private queries: Q;
|
|
313
310
|
private mutations: M;
|
|
314
311
|
private entities: EntitiesMap;
|
|
315
|
-
private
|
|
312
|
+
private resolverMap?: ResolverMap;
|
|
316
313
|
private contextFactory: (req?: unknown) => TContext | Promise<TContext>;
|
|
317
314
|
private version: string;
|
|
315
|
+
private logger: LensLogger;
|
|
318
316
|
private ctx = createContext<TContext>();
|
|
319
317
|
|
|
320
318
|
/** GraphStateManager for per-client state tracking */
|
|
@@ -350,9 +348,11 @@ class LensServerImpl<
|
|
|
350
348
|
this.queries = queries as Q;
|
|
351
349
|
this.mutations = mutations as M;
|
|
352
350
|
this.entities = config.entities ?? {};
|
|
353
|
-
|
|
351
|
+
// Normalize resolvers input (array or registry) to internal map
|
|
352
|
+
this.resolverMap = config.resolvers ? toResolverMap(config.resolvers) : undefined;
|
|
354
353
|
this.contextFactory = config.context ?? (() => ({}) as TContext);
|
|
355
354
|
this.version = config.version ?? "1.0.0";
|
|
355
|
+
this.logger = config.logger ?? noopLogger;
|
|
356
356
|
|
|
357
357
|
// Inject entity names from keys (if not already set)
|
|
358
358
|
for (const [name, def] of Object.entries(this.entities)) {
|
|
@@ -475,7 +475,7 @@ class LensServerImpl<
|
|
|
475
475
|
};
|
|
476
476
|
|
|
477
477
|
// Add queries
|
|
478
|
-
for (const [name,
|
|
478
|
+
for (const [name, _def] of Object.entries(this.queries)) {
|
|
479
479
|
setNested(name, { type: "query" });
|
|
480
480
|
}
|
|
481
481
|
|
|
@@ -787,7 +787,7 @@ class LensServerImpl<
|
|
|
787
787
|
try {
|
|
788
788
|
cleanup();
|
|
789
789
|
} catch (e) {
|
|
790
|
-
|
|
790
|
+
this.logger.error?.("Cleanup error:", e);
|
|
791
791
|
}
|
|
792
792
|
}
|
|
793
793
|
|
|
@@ -869,7 +869,7 @@ class LensServerImpl<
|
|
|
869
869
|
try {
|
|
870
870
|
cleanup();
|
|
871
871
|
} catch (e) {
|
|
872
|
-
|
|
872
|
+
this.logger.error?.("Cleanup error:", e);
|
|
873
873
|
}
|
|
874
874
|
}
|
|
875
875
|
}
|
|
@@ -1074,7 +1074,7 @@ class LensServerImpl<
|
|
|
1074
1074
|
},
|
|
1075
1075
|
});
|
|
1076
1076
|
|
|
1077
|
-
|
|
1077
|
+
this.logger.info?.(`Lens server listening on port ${port}`);
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
1080
|
async close(): Promise<void> {
|
|
@@ -1227,42 +1227,41 @@ class LensServerImpl<
|
|
|
1227
1227
|
|
|
1228
1228
|
/**
|
|
1229
1229
|
* Execute entity resolvers for nested data.
|
|
1230
|
-
* Processes the selection object and resolves relation fields.
|
|
1230
|
+
* Processes the selection object and resolves relation fields using new resolver() pattern.
|
|
1231
1231
|
*/
|
|
1232
1232
|
private async executeEntityResolvers<T>(
|
|
1233
1233
|
entityName: string,
|
|
1234
1234
|
data: T,
|
|
1235
1235
|
select?: SelectionObject,
|
|
1236
1236
|
): Promise<T> {
|
|
1237
|
-
if (!data || !select || !this.
|
|
1237
|
+
if (!data || !select || !this.resolverMap) return data;
|
|
1238
|
+
|
|
1239
|
+
// Get resolver for this entity
|
|
1240
|
+
const resolverDef = this.resolverMap.get(entityName);
|
|
1241
|
+
if (!resolverDef) return data;
|
|
1238
1242
|
|
|
1239
1243
|
const result = { ...(data as Record<string, unknown>) };
|
|
1244
|
+
const context = await this.contextFactory();
|
|
1240
1245
|
|
|
1241
1246
|
for (const [fieldName, fieldSelect] of Object.entries(select)) {
|
|
1242
1247
|
if (fieldSelect === false || fieldSelect === true) continue;
|
|
1243
1248
|
|
|
1244
|
-
// Check if this field has
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
const loader = this.loaders.get(loaderKey)!;
|
|
1261
|
-
result[fieldName] = await loader.load(data);
|
|
1262
|
-
} else {
|
|
1263
|
-
// Simple resolver
|
|
1264
|
-
result[fieldName] = await resolver(data);
|
|
1265
|
-
}
|
|
1249
|
+
// Check if this field has a resolver
|
|
1250
|
+
if (!resolverDef.hasField(fieldName)) continue;
|
|
1251
|
+
|
|
1252
|
+
// Extract field args from selection
|
|
1253
|
+
const fieldArgs =
|
|
1254
|
+
typeof fieldSelect === "object" && fieldSelect !== null && "args" in fieldSelect
|
|
1255
|
+
? ((fieldSelect as { args?: Record<string, unknown> }).args ?? {})
|
|
1256
|
+
: {};
|
|
1257
|
+
|
|
1258
|
+
// Execute field resolver with args
|
|
1259
|
+
result[fieldName] = await resolverDef.resolveField(
|
|
1260
|
+
fieldName,
|
|
1261
|
+
data as any,
|
|
1262
|
+
fieldArgs,
|
|
1263
|
+
context as any,
|
|
1264
|
+
);
|
|
1266
1265
|
|
|
1267
1266
|
// Recursively resolve nested selections
|
|
1268
1267
|
const nestedSelect = (fieldSelect as { select?: SelectionObject }).select;
|
|
@@ -1376,7 +1375,7 @@ class LensServerImpl<
|
|
|
1376
1375
|
value,
|
|
1377
1376
|
);
|
|
1378
1377
|
} catch (error) {
|
|
1379
|
-
|
|
1378
|
+
this.logger.warn?.(`Failed to serialize field ${entityName}.${fieldName}:`, error);
|
|
1380
1379
|
result[fieldName] = value;
|
|
1381
1380
|
}
|
|
1382
1381
|
} else {
|
|
@@ -1408,7 +1407,7 @@ class LensServerImpl<
|
|
|
1408
1407
|
let result = item;
|
|
1409
1408
|
|
|
1410
1409
|
// Execute entity resolvers for nested data
|
|
1411
|
-
if (select && this.
|
|
1410
|
+
if (select && this.resolverMap) {
|
|
1412
1411
|
result = await this.executeEntityResolvers(entityName, item, select);
|
|
1413
1412
|
}
|
|
1414
1413
|
|
|
@@ -1432,7 +1431,7 @@ class LensServerImpl<
|
|
|
1432
1431
|
let result: T = data;
|
|
1433
1432
|
|
|
1434
1433
|
// Execute entity resolvers for nested data
|
|
1435
|
-
if (select && this.
|
|
1434
|
+
if (select && this.resolverMap) {
|
|
1436
1435
|
result = (await this.executeEntityResolvers(entityName, data, select)) as T;
|
|
1437
1436
|
}
|
|
1438
1437
|
|
|
@@ -1512,22 +1511,20 @@ function isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> {
|
|
|
1512
1511
|
/**
|
|
1513
1512
|
* Infer input type from a query/mutation definition
|
|
1514
1513
|
*/
|
|
1515
|
-
export type InferInput<T> =
|
|
1516
|
-
|
|
1517
|
-
? void
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1514
|
+
export type InferInput<T> =
|
|
1515
|
+
T extends QueryDef<infer I, unknown>
|
|
1516
|
+
? I extends void
|
|
1517
|
+
? void
|
|
1518
|
+
: I
|
|
1519
|
+
: T extends MutationDef<infer I, unknown>
|
|
1520
|
+
? I
|
|
1521
|
+
: never;
|
|
1522
1522
|
|
|
1523
1523
|
/**
|
|
1524
1524
|
* Infer output type from a query/mutation definition
|
|
1525
1525
|
*/
|
|
1526
|
-
export type InferOutput<T> =
|
|
1527
|
-
? O
|
|
1528
|
-
: T extends MutationDef<unknown, infer O>
|
|
1529
|
-
? O
|
|
1530
|
-
: never;
|
|
1526
|
+
export type InferOutput<T> =
|
|
1527
|
+
T extends QueryDef<unknown, infer O> ? O : T extends MutationDef<unknown, infer O> ? O : never;
|
|
1531
1528
|
|
|
1532
1529
|
/**
|
|
1533
1530
|
* API type for client inference
|
|
@@ -1544,9 +1541,7 @@ export type InferOutput<T> = T extends QueryDef<unknown, infer O>
|
|
|
1544
1541
|
* const client = createClient<Api>({ links: [...] });
|
|
1545
1542
|
* ```
|
|
1546
1543
|
*/
|
|
1547
|
-
export type InferApi<T
|
|
1548
|
-
? { queries: Q; mutations: M }
|
|
1549
|
-
: never;
|
|
1544
|
+
export type InferApi<T> = T extends { _types: infer Types } ? Types : never;
|
|
1550
1545
|
|
|
1551
1546
|
// =============================================================================
|
|
1552
1547
|
// Factory
|
|
@@ -1561,11 +1556,11 @@ export type ServerConfigWithInferredContext<
|
|
|
1561
1556
|
M extends MutationsMap = MutationsMap,
|
|
1562
1557
|
> = {
|
|
1563
1558
|
entities?: EntitiesMap;
|
|
1564
|
-
relations?: RelationsArray;
|
|
1565
1559
|
router: TRouter;
|
|
1566
1560
|
queries?: Q;
|
|
1567
1561
|
mutations?: M;
|
|
1568
|
-
resolvers
|
|
1562
|
+
/** Field resolvers array */
|
|
1563
|
+
resolvers?: Resolvers;
|
|
1569
1564
|
/** Context factory - type is inferred from router's procedures */
|
|
1570
1565
|
context?: (req?: unknown) => InferRouterContext<TRouter> | Promise<InferRouterContext<TRouter>>;
|
|
1571
1566
|
version?: string;
|
|
@@ -1580,11 +1575,11 @@ export type ServerConfigLegacy<
|
|
|
1580
1575
|
M extends MutationsMap = MutationsMap,
|
|
1581
1576
|
> = {
|
|
1582
1577
|
entities?: EntitiesMap;
|
|
1583
|
-
relations?: RelationsArray;
|
|
1584
1578
|
router?: undefined;
|
|
1585
1579
|
queries?: Q;
|
|
1586
1580
|
mutations?: M;
|
|
1587
|
-
resolvers
|
|
1581
|
+
/** Field resolvers array */
|
|
1582
|
+
resolvers?: Resolvers;
|
|
1588
1583
|
context?: (req?: unknown) => TContext | Promise<TContext>;
|
|
1589
1584
|
version?: string;
|
|
1590
1585
|
};
|
|
@@ -1613,7 +1608,9 @@ export function createServer<
|
|
|
1613
1608
|
M extends MutationsMap = MutationsMap,
|
|
1614
1609
|
>(
|
|
1615
1610
|
config: ServerConfigWithInferredContext<TRouter, Q, M>,
|
|
1616
|
-
): LensServer & {
|
|
1611
|
+
): LensServer & {
|
|
1612
|
+
_types: { router: TRouter; queries: Q; mutations: M; context: InferRouterContext<TRouter> };
|
|
1613
|
+
};
|
|
1617
1614
|
|
|
1618
1615
|
export function createServer<
|
|
1619
1616
|
TContext extends ContextValue = ContextValue,
|
package/src/sse/handler.ts
CHANGED
|
@@ -69,7 +69,7 @@ export class SSEHandler {
|
|
|
69
69
|
* Handle new SSE connection
|
|
70
70
|
* Returns a Response with SSE stream
|
|
71
71
|
*/
|
|
72
|
-
handleConnection(
|
|
72
|
+
handleConnection(_req?: Request): Response {
|
|
73
73
|
const clientId = `sse_${++this.clientCounter}_${Date.now()}`;
|
|
74
74
|
const encoder = new TextEncoder();
|
|
75
75
|
|