@sylphx/lens-server 2.6.1 → 2.7.1

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/dist/index.js CHANGED
@@ -36,12 +36,13 @@ function extendContext(current, extension) {
36
36
  // src/server/create.ts
37
37
  import {
38
38
  createEmit,
39
+ createResolverFromEntity,
39
40
  flattenRouter,
40
41
  hashValue,
42
+ hasInlineResolvers,
41
43
  isEntityDef,
42
44
  isMutationDef,
43
45
  isQueryDef,
44
- toResolverMap,
45
46
  valuesEqual
46
47
  } from "@sylphx/lens-core";
47
48
 
@@ -353,7 +354,6 @@ class LensServerImpl {
353
354
  }
354
355
  this.queries = queries;
355
356
  this.mutations = mutations;
356
- this.resolverMap = config.resolvers ? toResolverMap(config.resolvers) : undefined;
357
357
  const entities = { ...config.entities ?? {} };
358
358
  if (config.resolvers) {
359
359
  for (const resolver of config.resolvers) {
@@ -364,6 +364,7 @@ class LensServerImpl {
364
364
  }
365
365
  }
366
366
  this.entities = entities;
367
+ this.resolverMap = this.buildResolverMap(config.resolvers, entities);
367
368
  this.contextFactory = config.context ?? (() => ({}));
368
369
  this.version = config.version ?? "1.0.0";
369
370
  this.logger = config.logger ?? noopLogger;
@@ -403,6 +404,28 @@ class LensServerImpl {
403
404
  }
404
405
  }
405
406
  }
407
+ buildResolverMap(explicitResolvers, entities) {
408
+ const resolverMap = new Map;
409
+ if (explicitResolvers) {
410
+ for (const resolver of explicitResolvers) {
411
+ const entityName = resolver.entity._name;
412
+ if (entityName) {
413
+ resolverMap.set(entityName, resolver);
414
+ }
415
+ }
416
+ }
417
+ for (const [name, entity] of Object.entries(entities)) {
418
+ if (!isEntityDef(entity))
419
+ continue;
420
+ if (resolverMap.has(name))
421
+ continue;
422
+ if (hasInlineResolvers(entity)) {
423
+ const resolver = createResolverFromEntity(entity);
424
+ resolverMap.set(name, resolver);
425
+ }
426
+ }
427
+ return resolverMap.size > 0 ? resolverMap : undefined;
428
+ }
406
429
  hasAnySubscription(entityName, select, visited = new Set) {
407
430
  if (visited.has(entityName))
408
431
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-server",
3
- "version": "2.6.1",
3
+ "version": "2.7.1",
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.4.0"
33
+ "@sylphx/lens-core": "^2.5.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "^5.9.3",
@@ -1477,3 +1477,125 @@ describe("Subscription detection", () => {
1477
1477
  });
1478
1478
  });
1479
1479
  });
1480
+
1481
+ // =============================================================================
1482
+ // Unified Entity Definition (ADR-001) - Auto Resolver Conversion
1483
+ // =============================================================================
1484
+
1485
+ describe("Unified Entity Definition", () => {
1486
+ describe("auto-converts entities with inline resolvers", () => {
1487
+ it("creates resolver from entity with inline .resolve()", async () => {
1488
+ // Entity with inline resolver - no separate resolver() needed
1489
+ const Product = entity("Product", (t) => ({
1490
+ id: t.id(),
1491
+ name: t.string(),
1492
+ price: t.float(),
1493
+ // Computed field with inline resolver
1494
+ displayPrice: t.string().resolve(({ parent }) => `$${(parent as { price: number }).price.toFixed(2)}`),
1495
+ }));
1496
+
1497
+ const getProduct = query()
1498
+ .input(z.object({ id: z.string() }))
1499
+ .returns(Product)
1500
+ .resolve(({ input }) => ({
1501
+ id: input.id,
1502
+ name: "Test Product",
1503
+ price: 19.99,
1504
+ }));
1505
+
1506
+ // No resolvers array needed! Server auto-detects inline resolvers
1507
+ const server = createApp({
1508
+ entities: { Product },
1509
+ queries: { getProduct },
1510
+ });
1511
+
1512
+ const result = await firstValueFrom(
1513
+ server.execute({
1514
+ path: "getProduct",
1515
+ input: { id: "prod-1", $select: { id: true, name: true, displayPrice: true } },
1516
+ }),
1517
+ );
1518
+
1519
+ expect(result.data).toEqual({
1520
+ id: "prod-1",
1521
+ name: "Test Product",
1522
+ displayPrice: "$19.99",
1523
+ });
1524
+ });
1525
+
1526
+ it("explicit resolver takes priority over inline resolver", async () => {
1527
+ // Entity with inline resolver
1528
+ const Item = entity("Item", (t) => ({
1529
+ id: t.id(),
1530
+ label: t.string().resolve(() => "inline-label"),
1531
+ }));
1532
+
1533
+ // Explicit resolver overrides inline
1534
+ const itemResolver = resolver(Item, (f) => ({
1535
+ id: f.expose("id"),
1536
+ label: f.string().resolve(() => "explicit-label"),
1537
+ }));
1538
+
1539
+ const getItem = query()
1540
+ .input(z.object({ id: z.string() }))
1541
+ .returns(Item)
1542
+ .resolve(({ input }) => ({ id: input.id }));
1543
+
1544
+ const server = createApp({
1545
+ entities: { Item },
1546
+ queries: { getItem },
1547
+ resolvers: [itemResolver], // Explicit resolver takes priority
1548
+ });
1549
+
1550
+ const result = await firstValueFrom(
1551
+ server.execute({
1552
+ path: "getItem",
1553
+ input: { id: "item-1", $select: { id: true, label: true } },
1554
+ }),
1555
+ );
1556
+
1557
+ expect(result.data).toEqual({
1558
+ id: "item-1",
1559
+ label: "explicit-label", // From explicit resolver, not inline
1560
+ });
1561
+ });
1562
+
1563
+ it("includes inline resolver fields in metadata", () => {
1564
+ const Task = entity("Task", (t) => ({
1565
+ id: t.id(),
1566
+ title: t.string(),
1567
+ status: t.string().subscribe(({ ctx }) => {
1568
+ ctx.emit("pending");
1569
+ }),
1570
+ }));
1571
+
1572
+ const server = createApp({
1573
+ entities: { Task },
1574
+ queries: {},
1575
+ });
1576
+
1577
+ const metadata = server.getMetadata();
1578
+ expect(metadata.entities.Task).toBeDefined();
1579
+ expect(metadata.entities.Task.id).toBe("exposed");
1580
+ expect(metadata.entities.Task.title).toBe("exposed");
1581
+ expect(metadata.entities.Task.status).toBe("subscribe");
1582
+ });
1583
+
1584
+ it("skips entities without inline resolvers", () => {
1585
+ // Plain entity without inline resolvers
1586
+ const SimpleUser = entity("SimpleUser", {
1587
+ id: t.id(),
1588
+ name: t.string(),
1589
+ });
1590
+
1591
+ const server = createApp({
1592
+ entities: { SimpleUser },
1593
+ queries: {},
1594
+ });
1595
+
1596
+ const metadata = server.getMetadata();
1597
+ // No resolver created for entities without inline resolvers
1598
+ expect(metadata.entities.SimpleUser).toBeUndefined();
1599
+ });
1600
+ });
1601
+ });
@@ -18,10 +18,12 @@
18
18
  import {
19
19
  type ContextValue,
20
20
  createEmit,
21
+ createResolverFromEntity,
21
22
  type EmitCommand,
22
23
  type EntityDef,
23
24
  flattenRouter,
24
25
  hashValue,
26
+ hasInlineResolvers,
25
27
  type InferRouterContext,
26
28
  isEntityDef,
27
29
  isMutationDef,
@@ -29,7 +31,6 @@ import {
29
31
  type Observable,
30
32
  type ResolverDef,
31
33
  type RouterDef,
32
- toResolverMap,
33
34
  valuesEqual,
34
35
  } from "@sylphx/lens-core";
35
36
  import { createContext, runWithContext, tryUseContext } from "../context/index.js";
@@ -189,7 +190,6 @@ class LensServerImpl<
189
190
 
190
191
  this.queries = queries as Q;
191
192
  this.mutations = mutations as M;
192
- this.resolverMap = config.resolvers ? toResolverMap(config.resolvers) : undefined;
193
193
 
194
194
  // Build entities map: explicit config + auto-extracted from resolvers
195
195
  const entities: EntitiesMap = { ...(config.entities ?? {}) };
@@ -202,6 +202,11 @@ class LensServerImpl<
202
202
  }
203
203
  }
204
204
  this.entities = entities;
205
+
206
+ // Build resolver map: explicit resolvers + auto-converted from entities with inline resolvers
207
+ // Unified Entity Definition (ADR-001): entities can have inline .resolve()/.subscribe() methods
208
+ // These are automatically converted to resolvers, no need to call createResolverFromEntity() manually
209
+ this.resolverMap = this.buildResolverMap(config.resolvers, entities);
205
210
  this.contextFactory = config.context ?? (() => ({}) as TContext);
206
211
  this.version = config.version ?? "1.0.0";
207
212
  this.logger = config.logger ?? noopLogger;
@@ -248,6 +253,45 @@ class LensServerImpl<
248
253
  }
249
254
  }
250
255
 
256
+ /**
257
+ * Build resolver map from explicit resolvers and entities with inline resolvers.
258
+ *
259
+ * Unified Entity Definition (ADR-001): Entities can have inline .resolve()/.subscribe() methods.
260
+ * These are automatically converted to resolvers - no manual createResolverFromEntity() needed.
261
+ *
262
+ * Priority: explicit resolvers > auto-converted from entities
263
+ */
264
+ private buildResolverMap(
265
+ explicitResolvers: import("@sylphx/lens-core").Resolvers | undefined,
266
+ entities: EntitiesMap,
267
+ ): ResolverMap | undefined {
268
+ const resolverMap: ResolverMap = new Map();
269
+
270
+ // 1. Add explicit resolvers first (takes priority)
271
+ if (explicitResolvers) {
272
+ for (const resolver of explicitResolvers) {
273
+ const entityName = resolver.entity._name;
274
+ if (entityName) {
275
+ resolverMap.set(entityName, resolver);
276
+ }
277
+ }
278
+ }
279
+
280
+ // 2. Auto-convert entities with inline resolvers (if not already in map)
281
+ for (const [name, entity] of Object.entries(entities)) {
282
+ if (!isEntityDef(entity)) continue;
283
+ if (resolverMap.has(name)) continue; // Explicit resolver takes priority
284
+
285
+ // Check if entity has inline resolvers
286
+ if (hasInlineResolvers(entity)) {
287
+ const resolver = createResolverFromEntity(entity);
288
+ resolverMap.set(name, resolver);
289
+ }
290
+ }
291
+
292
+ return resolverMap.size > 0 ? resolverMap : undefined;
293
+ }
294
+
251
295
  // =========================================================================
252
296
  // Subscription Detection (Deprecated - Use client-side with entities metadata)
253
297
  // =========================================================================