@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.
@@ -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 RelationDef,
23
- type RelationTypeWithForeignKey,
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
- /** Relations array type */
58
- export type RelationsArray = RelationDef<
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
- /** Entity resolvers */
90
- resolvers?: EntityResolvers<EntityResolversDefinition>;
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
- callbacks.forEach(({ resolve }) => resolve(result));
282
+ for (const { resolve } of callbacks) resolve(result);
289
283
  });
290
284
  } catch (error) {
291
285
  for (const callbacks of batch.values()) {
292
- callbacks.forEach(({ reject }) => reject(error as Error));
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 resolvers?: EntityResolvers<EntityResolversDefinition>;
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
- this.resolvers = config.resolvers;
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, def] of Object.entries(this.queries)) {
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
- console.error("Cleanup error:", e);
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
- console.error("Cleanup error:", e);
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
- console.log(`Lens server listening on port ${port}`);
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.resolvers) return data;
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 an entity resolver
1245
- const resolver = this.resolvers.getResolver(entityName, fieldName);
1246
- if (!resolver) continue;
1247
-
1248
- // Execute resolver (with batching if available)
1249
- if (isBatchResolver(resolver)) {
1250
- // Use DataLoader for batching
1251
- const loaderKey = `${entityName}.${fieldName}`;
1252
- if (!this.loaders.has(loaderKey)) {
1253
- this.loaders.set(
1254
- loaderKey,
1255
- new DataLoader(async (parents: unknown[]) => {
1256
- return resolver.batch(parents);
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
- console.warn(`Failed to serialize field ${entityName}.${fieldName}:`, error);
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.resolvers) {
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.resolvers) {
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> = 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;
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> = T extends QueryDef<unknown, infer O>
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 extends LensServer> = T extends LensServerImpl<infer Q, infer M>
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?: EntityResolvers<EntityResolversDefinition>;
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?: EntityResolvers<EntityResolversDefinition>;
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 & { _types: { queries: Q; mutations: M; context: InferRouterContext<TRouter> } };
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,
@@ -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(req?: Request): Response {
72
+ handleConnection(_req?: Request): Response {
73
73
  const clientId = `sse_${++this.clientCounter}_${Date.now()}`;
74
74
  const encoder = new TextEncoder();
75
75