@sylphx/lens-server 2.5.0 → 2.6.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.d.ts +27 -2
- package/dist/index.js +42 -1
- package/package.json +2 -2
- package/src/e2e/server.test.ts +2 -1
- package/src/server/create.test.ts +5 -3
- package/src/server/create.ts +66 -2
- package/src/server/types.ts +31 -3
package/dist/index.d.ts
CHANGED
|
@@ -468,6 +468,11 @@ type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
|
|
|
468
468
|
interface OperationMeta {
|
|
469
469
|
type: "query" | "mutation" | "subscription";
|
|
470
470
|
optimistic?: OptimisticDSL;
|
|
471
|
+
/**
|
|
472
|
+
* Return entity type name (if operation returns an entity).
|
|
473
|
+
* Used by client for field-level subscription detection.
|
|
474
|
+
*/
|
|
475
|
+
returnType?: string;
|
|
471
476
|
}
|
|
472
477
|
/** Nested operations structure for handshake */
|
|
473
478
|
type OperationsMap = {
|
|
@@ -506,10 +511,25 @@ interface LensServerConfig<
|
|
|
506
511
|
*/
|
|
507
512
|
plugins?: ServerPlugin[] | undefined;
|
|
508
513
|
}
|
|
514
|
+
/** Field mode for entity fields */
|
|
515
|
+
type FieldMode = "exposed" | "resolve" | "subscribe";
|
|
516
|
+
/** Entity field metadata for client-side routing decisions */
|
|
517
|
+
interface EntityFieldMetadata {
|
|
518
|
+
[fieldName: string]: FieldMode;
|
|
519
|
+
}
|
|
520
|
+
/** All entities field metadata */
|
|
521
|
+
interface EntitiesMetadata {
|
|
522
|
+
[entityName: string]: EntityFieldMetadata;
|
|
523
|
+
}
|
|
509
524
|
/** Server metadata for transport handshake */
|
|
510
525
|
interface ServerMetadata {
|
|
511
526
|
version: string;
|
|
512
527
|
operations: OperationsMap;
|
|
528
|
+
/**
|
|
529
|
+
* Entity field metadata for client-side transport routing.
|
|
530
|
+
* Client uses this to determine if any selected field requires streaming transport.
|
|
531
|
+
*/
|
|
532
|
+
entities: EntitiesMetadata;
|
|
513
533
|
}
|
|
514
534
|
/** Operation for execution */
|
|
515
535
|
interface LensOperation {
|
|
@@ -614,7 +634,9 @@ interface LensServer {
|
|
|
614
634
|
getPluginManager(): PluginManager;
|
|
615
635
|
/**
|
|
616
636
|
* Check if any selected field (recursively) is a subscription.
|
|
617
|
-
*
|
|
637
|
+
*
|
|
638
|
+
* @deprecated Use client-side subscription detection with `getMetadata().entities` instead.
|
|
639
|
+
* The client should use the entities metadata to determine transport routing.
|
|
618
640
|
*
|
|
619
641
|
* @param entityName - The entity type name
|
|
620
642
|
* @param select - Selection object (if undefined, checks ALL fields)
|
|
@@ -623,7 +645,10 @@ interface LensServer {
|
|
|
623
645
|
hasAnySubscription(entityName: string, select?: SelectionObject): boolean;
|
|
624
646
|
/**
|
|
625
647
|
* Check if an operation requires streaming transport.
|
|
626
|
-
*
|
|
648
|
+
*
|
|
649
|
+
* @deprecated Use client-side transport detection with `getMetadata().entities` instead.
|
|
650
|
+
* The client should determine transport based on operation type from metadata
|
|
651
|
+
* and entity field modes.
|
|
627
652
|
*
|
|
628
653
|
* @param path - Operation path
|
|
629
654
|
* @param select - Selection object for return type fields
|
package/dist/index.js
CHANGED
|
@@ -454,9 +454,28 @@ class LensServerImpl {
|
|
|
454
454
|
getMetadata() {
|
|
455
455
|
return {
|
|
456
456
|
version: this.version,
|
|
457
|
-
operations: this.buildOperationsMap()
|
|
457
|
+
operations: this.buildOperationsMap(),
|
|
458
|
+
entities: this.buildEntitiesMetadata()
|
|
458
459
|
};
|
|
459
460
|
}
|
|
461
|
+
buildEntitiesMetadata() {
|
|
462
|
+
const result = {};
|
|
463
|
+
if (!this.resolverMap)
|
|
464
|
+
return result;
|
|
465
|
+
for (const [entityName, resolver] of this.resolverMap) {
|
|
466
|
+
const fieldMetadata = {};
|
|
467
|
+
for (const fieldName of resolver.getFieldNames()) {
|
|
468
|
+
const mode = resolver.getFieldMode(String(fieldName));
|
|
469
|
+
if (mode) {
|
|
470
|
+
fieldMetadata[String(fieldName)] = mode;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (Object.keys(fieldMetadata).length > 0) {
|
|
474
|
+
result[entityName] = fieldMetadata;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
460
479
|
execute(op) {
|
|
461
480
|
const { path, input } = op;
|
|
462
481
|
const isQuery = !!this.queries[path];
|
|
@@ -817,10 +836,28 @@ class LensServerImpl {
|
|
|
817
836
|
}
|
|
818
837
|
current[parts[parts.length - 1]] = meta;
|
|
819
838
|
};
|
|
839
|
+
const getReturnTypeName = (output) => {
|
|
840
|
+
if (!output)
|
|
841
|
+
return;
|
|
842
|
+
if (Array.isArray(output) && output.length > 0) {
|
|
843
|
+
const element = output[0];
|
|
844
|
+
if (element && typeof element === "object" && "_name" in element) {
|
|
845
|
+
return element._name;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (typeof output === "object" && "_name" in output) {
|
|
849
|
+
return output._name;
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
};
|
|
820
853
|
for (const [name, def] of Object.entries(this.queries)) {
|
|
821
854
|
const isSubscription = def._resolve?.constructor?.name === "AsyncGeneratorFunction" || def._resolve?.constructor?.name === "GeneratorFunction";
|
|
822
855
|
const opType = isSubscription ? "subscription" : "query";
|
|
856
|
+
const returnType = getReturnTypeName(def._output);
|
|
823
857
|
const meta = { type: opType };
|
|
858
|
+
if (returnType) {
|
|
859
|
+
meta.returnType = returnType;
|
|
860
|
+
}
|
|
824
861
|
this.pluginManager.runEnhanceOperationMeta({
|
|
825
862
|
path: name,
|
|
826
863
|
type: opType,
|
|
@@ -830,7 +867,11 @@ class LensServerImpl {
|
|
|
830
867
|
setNested(name, meta);
|
|
831
868
|
}
|
|
832
869
|
for (const [name, def] of Object.entries(this.mutations)) {
|
|
870
|
+
const returnType = getReturnTypeName(def._output);
|
|
833
871
|
const meta = { type: "mutation" };
|
|
872
|
+
if (returnType) {
|
|
873
|
+
meta.returnType = returnType;
|
|
874
|
+
}
|
|
834
875
|
this.pluginManager.runEnhanceOperationMeta({
|
|
835
876
|
path: name,
|
|
836
877
|
type: "mutation",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/lens-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.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.
|
|
33
|
+
"@sylphx/lens-core": "^2.4.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.9.3",
|
package/src/e2e/server.test.ts
CHANGED
|
@@ -448,7 +448,8 @@ describe("E2E - Metadata", () => {
|
|
|
448
448
|
const metadata = server.getMetadata();
|
|
449
449
|
|
|
450
450
|
expect(metadata.version).toBe("2.0.0");
|
|
451
|
-
expect(metadata.operations.getUser).
|
|
451
|
+
expect(metadata.operations.getUser.type).toBe("query");
|
|
452
|
+
expect(metadata.operations.getUser.returnType).toBe("User"); // Now includes returnType
|
|
452
453
|
expect(metadata.operations.createUser.type).toBe("mutation");
|
|
453
454
|
// createUser should have auto-derived optimistic hint (with plugin)
|
|
454
455
|
expect(metadata.operations.createUser.optimistic).toBeDefined();
|
|
@@ -127,8 +127,9 @@ describe("getMetadata", () => {
|
|
|
127
127
|
const metadata = server.getMetadata();
|
|
128
128
|
|
|
129
129
|
expect(metadata.version).toBe("1.2.3");
|
|
130
|
-
expect(metadata.operations.getUser).
|
|
131
|
-
expect(metadata.operations.
|
|
130
|
+
expect(metadata.operations.getUser.type).toBe("query");
|
|
131
|
+
expect(metadata.operations.getUser.returnType).toBe("User"); // Now includes returnType
|
|
132
|
+
expect(metadata.operations.getUsers.type).toBe("query");
|
|
132
133
|
expect(metadata.operations.createUser.type).toBe("mutation");
|
|
133
134
|
expect(metadata.operations.updateUser.type).toBe("mutation");
|
|
134
135
|
});
|
|
@@ -175,7 +176,8 @@ describe("getMetadata", () => {
|
|
|
175
176
|
const metadata = server.getMetadata();
|
|
176
177
|
|
|
177
178
|
expect(metadata.operations.user).toBeDefined();
|
|
178
|
-
expect((metadata.operations.user as Record<string, unknown
|
|
179
|
+
expect((metadata.operations.user as Record<string, Record<string, unknown>>).get.type).toBe("query");
|
|
180
|
+
expect((metadata.operations.user as Record<string, Record<string, unknown>>).get.returnType).toBe("User");
|
|
179
181
|
expect((metadata.operations.user as Record<string, unknown>).create).toBeDefined();
|
|
180
182
|
});
|
|
181
183
|
});
|
package/src/server/create.ts
CHANGED
|
@@ -47,6 +47,8 @@ import { applySelection, extractNestedInputs } from "./selection.js";
|
|
|
47
47
|
import type {
|
|
48
48
|
ClientSendFn,
|
|
49
49
|
EntitiesMap,
|
|
50
|
+
EntitiesMetadata,
|
|
51
|
+
EntityFieldMetadata,
|
|
50
52
|
LensLogger,
|
|
51
53
|
LensOperation,
|
|
52
54
|
LensResult,
|
|
@@ -66,6 +68,8 @@ import type {
|
|
|
66
68
|
export type {
|
|
67
69
|
ClientSendFn,
|
|
68
70
|
EntitiesMap,
|
|
71
|
+
EntitiesMetadata,
|
|
72
|
+
EntityFieldMetadata,
|
|
69
73
|
InferApi,
|
|
70
74
|
InferInput,
|
|
71
75
|
InferOutput,
|
|
@@ -245,12 +249,15 @@ class LensServerImpl<
|
|
|
245
249
|
}
|
|
246
250
|
|
|
247
251
|
// =========================================================================
|
|
248
|
-
// Subscription Detection
|
|
252
|
+
// Subscription Detection (Deprecated - Use client-side with entities metadata)
|
|
249
253
|
// =========================================================================
|
|
250
254
|
|
|
251
255
|
/**
|
|
252
256
|
* Check if any selected field (recursively) is a subscription.
|
|
253
|
-
*
|
|
257
|
+
*
|
|
258
|
+
* @deprecated Use client-side subscription detection with `getMetadata().entities` instead.
|
|
259
|
+
* The client should use the entities metadata to determine transport routing.
|
|
260
|
+
* This method remains for backwards compatibility but will be removed in a future version.
|
|
254
261
|
*
|
|
255
262
|
* @param entityName - The entity type name
|
|
256
263
|
* @param select - Selection object (if undefined, checks ALL fields)
|
|
@@ -310,6 +317,10 @@ class LensServerImpl<
|
|
|
310
317
|
/**
|
|
311
318
|
* Check if an operation (and its return type's fields) requires streaming transport.
|
|
312
319
|
*
|
|
320
|
+
* @deprecated Use client-side transport detection with `getMetadata().entities` instead.
|
|
321
|
+
* The client should determine transport based on operation type from metadata
|
|
322
|
+
* and entity field modes. This method remains for backwards compatibility.
|
|
323
|
+
*
|
|
313
324
|
* Returns true if:
|
|
314
325
|
* 1. Operation resolver is async generator (yields values)
|
|
315
326
|
* 2. Operation resolver uses emit pattern
|
|
@@ -352,9 +363,37 @@ class LensServerImpl<
|
|
|
352
363
|
return {
|
|
353
364
|
version: this.version,
|
|
354
365
|
operations: this.buildOperationsMap(),
|
|
366
|
+
entities: this.buildEntitiesMetadata(),
|
|
355
367
|
};
|
|
356
368
|
}
|
|
357
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Build entities metadata for client-side transport routing.
|
|
372
|
+
* Maps each entity to its field modes (exposed/resolve/subscribe).
|
|
373
|
+
*/
|
|
374
|
+
private buildEntitiesMetadata(): EntitiesMetadata {
|
|
375
|
+
const result: EntitiesMetadata = {};
|
|
376
|
+
|
|
377
|
+
if (!this.resolverMap) return result;
|
|
378
|
+
|
|
379
|
+
for (const [entityName, resolver] of this.resolverMap) {
|
|
380
|
+
const fieldMetadata: EntityFieldMetadata = {};
|
|
381
|
+
|
|
382
|
+
for (const fieldName of resolver.getFieldNames()) {
|
|
383
|
+
const mode = resolver.getFieldMode(String(fieldName));
|
|
384
|
+
if (mode) {
|
|
385
|
+
fieldMetadata[String(fieldName)] = mode;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (Object.keys(fieldMetadata).length > 0) {
|
|
390
|
+
result[entityName] = fieldMetadata;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
|
|
358
397
|
/**
|
|
359
398
|
* Execute operation and return Observable.
|
|
360
399
|
*
|
|
@@ -996,13 +1035,34 @@ class LensServerImpl<
|
|
|
996
1035
|
current[parts[parts.length - 1]] = meta;
|
|
997
1036
|
};
|
|
998
1037
|
|
|
1038
|
+
// Helper to extract return type name from output definition
|
|
1039
|
+
const getReturnTypeName = (output: unknown): string | undefined => {
|
|
1040
|
+
if (!output) return undefined;
|
|
1041
|
+
// Handle array output: [EntityDef] → extract from first element
|
|
1042
|
+
if (Array.isArray(output) && output.length > 0) {
|
|
1043
|
+
const element = output[0];
|
|
1044
|
+
if (element && typeof element === "object" && "_name" in element) {
|
|
1045
|
+
return element._name as string;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// Handle direct entity output
|
|
1049
|
+
if (typeof output === "object" && "_name" in output) {
|
|
1050
|
+
return (output as { _name?: string })._name;
|
|
1051
|
+
}
|
|
1052
|
+
return undefined;
|
|
1053
|
+
};
|
|
1054
|
+
|
|
999
1055
|
for (const [name, def] of Object.entries(this.queries)) {
|
|
1000
1056
|
// Auto-detect subscription: if resolver is AsyncGeneratorFunction → subscription
|
|
1001
1057
|
const isSubscription =
|
|
1002
1058
|
def._resolve?.constructor?.name === "AsyncGeneratorFunction" ||
|
|
1003
1059
|
def._resolve?.constructor?.name === "GeneratorFunction";
|
|
1004
1060
|
const opType = isSubscription ? "subscription" : "query";
|
|
1061
|
+
const returnType = getReturnTypeName(def._output);
|
|
1005
1062
|
const meta: OperationMeta = { type: opType };
|
|
1063
|
+
if (returnType) {
|
|
1064
|
+
meta.returnType = returnType;
|
|
1065
|
+
}
|
|
1006
1066
|
this.pluginManager.runEnhanceOperationMeta({
|
|
1007
1067
|
path: name,
|
|
1008
1068
|
type: opType,
|
|
@@ -1013,7 +1073,11 @@ class LensServerImpl<
|
|
|
1013
1073
|
}
|
|
1014
1074
|
|
|
1015
1075
|
for (const [name, def] of Object.entries(this.mutations)) {
|
|
1076
|
+
const returnType = getReturnTypeName(def._output);
|
|
1016
1077
|
const meta: OperationMeta = { type: "mutation" };
|
|
1078
|
+
if (returnType) {
|
|
1079
|
+
meta.returnType = returnType;
|
|
1080
|
+
}
|
|
1017
1081
|
this.pluginManager.runEnhanceOperationMeta({
|
|
1018
1082
|
path: name,
|
|
1019
1083
|
type: "mutation",
|
package/src/server/types.ts
CHANGED
|
@@ -64,6 +64,11 @@ export type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
|
|
|
64
64
|
export interface OperationMeta {
|
|
65
65
|
type: "query" | "mutation" | "subscription";
|
|
66
66
|
optimistic?: OptimisticDSL;
|
|
67
|
+
/**
|
|
68
|
+
* Return entity type name (if operation returns an entity).
|
|
69
|
+
* Used by client for field-level subscription detection.
|
|
70
|
+
*/
|
|
71
|
+
returnType?: string;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
/** Nested operations structure for handshake */
|
|
@@ -118,10 +123,28 @@ export interface LensServerConfig<
|
|
|
118
123
|
// Server Metadata
|
|
119
124
|
// =============================================================================
|
|
120
125
|
|
|
126
|
+
/** Field mode for entity fields */
|
|
127
|
+
export type FieldMode = "exposed" | "resolve" | "subscribe";
|
|
128
|
+
|
|
129
|
+
/** Entity field metadata for client-side routing decisions */
|
|
130
|
+
export interface EntityFieldMetadata {
|
|
131
|
+
[fieldName: string]: FieldMode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** All entities field metadata */
|
|
135
|
+
export interface EntitiesMetadata {
|
|
136
|
+
[entityName: string]: EntityFieldMetadata;
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
/** Server metadata for transport handshake */
|
|
122
140
|
export interface ServerMetadata {
|
|
123
141
|
version: string;
|
|
124
142
|
operations: OperationsMap;
|
|
143
|
+
/**
|
|
144
|
+
* Entity field metadata for client-side transport routing.
|
|
145
|
+
* Client uses this to determine if any selected field requires streaming transport.
|
|
146
|
+
*/
|
|
147
|
+
entities: EntitiesMetadata;
|
|
125
148
|
}
|
|
126
149
|
|
|
127
150
|
// =============================================================================
|
|
@@ -264,12 +287,14 @@ export interface LensServer {
|
|
|
264
287
|
getPluginManager(): PluginManager;
|
|
265
288
|
|
|
266
289
|
// =========================================================================
|
|
267
|
-
// Subscription Detection (
|
|
290
|
+
// Subscription Detection (Deprecated - Use client-side with entities metadata)
|
|
268
291
|
// =========================================================================
|
|
269
292
|
|
|
270
293
|
/**
|
|
271
294
|
* Check if any selected field (recursively) is a subscription.
|
|
272
|
-
*
|
|
295
|
+
*
|
|
296
|
+
* @deprecated Use client-side subscription detection with `getMetadata().entities` instead.
|
|
297
|
+
* The client should use the entities metadata to determine transport routing.
|
|
273
298
|
*
|
|
274
299
|
* @param entityName - The entity type name
|
|
275
300
|
* @param select - Selection object (if undefined, checks ALL fields)
|
|
@@ -279,7 +304,10 @@ export interface LensServer {
|
|
|
279
304
|
|
|
280
305
|
/**
|
|
281
306
|
* Check if an operation requires streaming transport.
|
|
282
|
-
*
|
|
307
|
+
*
|
|
308
|
+
* @deprecated Use client-side transport detection with `getMetadata().entities` instead.
|
|
309
|
+
* The client should determine transport based on operation type from metadata
|
|
310
|
+
* and entity field modes.
|
|
283
311
|
*
|
|
284
312
|
* @param path - Operation path
|
|
285
313
|
* @param select - Selection object for return type fields
|