@sylphx/lens-server 2.6.1 → 2.7.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/dist/index.js +25 -2
- package/package.json +1 -1
- package/src/server/create.test.ts +122 -0
- package/src/server/create.ts +46 -2
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
|
@@ -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
|
+
});
|
package/src/server/create.ts
CHANGED
|
@@ -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
|
// =========================================================================
|