@sylphx/lens-server 2.0.1 → 2.2.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.d.ts +32 -5
- package/dist/index.js +189 -89
- package/package.json +2 -2
- package/src/e2e/server.test.ts +81 -61
- package/src/handlers/framework.ts +4 -3
- package/src/handlers/http.ts +7 -4
- package/src/handlers/ws.ts +18 -10
- package/src/server/create.test.ts +69 -47
- package/src/server/create.ts +198 -91
- package/src/server/selection.ts +82 -3
- package/src/server/types.ts +34 -4
package/src/server/selection.ts
CHANGED
|
@@ -4,12 +4,53 @@
|
|
|
4
4
|
* Field selection logic for query results.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { SelectionObject } from "./types.js";
|
|
7
|
+
import type { NestedSelection, SelectionObject } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if a value is a NestedSelection with input.
|
|
11
|
+
*/
|
|
12
|
+
function isNestedSelection(value: unknown): value is NestedSelection {
|
|
13
|
+
return (
|
|
14
|
+
typeof value === "object" &&
|
|
15
|
+
value !== null &&
|
|
16
|
+
("input" in value || "select" in value) &&
|
|
17
|
+
!Array.isArray(value)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract the select object from a selection value.
|
|
23
|
+
* Handles: true, SelectionObject, { select: ... }, { input: ..., select: ... }
|
|
24
|
+
*/
|
|
25
|
+
function extractSelect(value: unknown): SelectionObject | null {
|
|
26
|
+
if (value === true) return null;
|
|
27
|
+
if (typeof value !== "object" || value === null) return null;
|
|
28
|
+
|
|
29
|
+
const obj = value as Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
// { input?: ..., select?: ... } pattern
|
|
32
|
+
if ("input" in obj || ("select" in obj && typeof obj.select === "object")) {
|
|
33
|
+
return (obj.select as SelectionObject) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// { select: ... } pattern (without input)
|
|
37
|
+
if ("select" in obj && typeof obj.select === "object") {
|
|
38
|
+
return obj.select as SelectionObject;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Direct SelectionObject
|
|
42
|
+
return value as SelectionObject;
|
|
43
|
+
}
|
|
8
44
|
|
|
9
45
|
/**
|
|
10
46
|
* Apply field selection to data.
|
|
11
47
|
* Recursively filters data to only include selected fields.
|
|
12
48
|
*
|
|
49
|
+
* Supports:
|
|
50
|
+
* - `field: true` - Include field as-is
|
|
51
|
+
* - `field: { select: {...} }` - Nested selection
|
|
52
|
+
* - `field: { input: {...}, select: {...} }` - Nested with input (input passed to resolvers)
|
|
53
|
+
*
|
|
13
54
|
* @param data - The data to filter
|
|
14
55
|
* @param select - Selection object specifying which fields to include
|
|
15
56
|
* @returns Filtered data with only selected fields
|
|
@@ -35,10 +76,48 @@ export function applySelection(data: unknown, select: SelectionObject): unknown
|
|
|
35
76
|
if (value === true) {
|
|
36
77
|
result[key] = obj[key];
|
|
37
78
|
} else if (typeof value === "object" && value !== null) {
|
|
38
|
-
const nestedSelect =
|
|
39
|
-
|
|
79
|
+
const nestedSelect = extractSelect(value);
|
|
80
|
+
if (nestedSelect) {
|
|
81
|
+
result[key] = applySelection(obj[key], nestedSelect);
|
|
82
|
+
} else {
|
|
83
|
+
// No nested select means include the whole field
|
|
84
|
+
result[key] = obj[key];
|
|
85
|
+
}
|
|
40
86
|
}
|
|
41
87
|
}
|
|
42
88
|
|
|
43
89
|
return result;
|
|
44
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract nested inputs from a selection object.
|
|
94
|
+
* Returns a map of field paths to their input params.
|
|
95
|
+
* Used by resolvers to fetch nested data with the right params.
|
|
96
|
+
*/
|
|
97
|
+
export function extractNestedInputs(
|
|
98
|
+
select: SelectionObject,
|
|
99
|
+
prefix = "",
|
|
100
|
+
): Map<string, Record<string, unknown>> {
|
|
101
|
+
const inputs = new Map<string, Record<string, unknown>>();
|
|
102
|
+
|
|
103
|
+
for (const [key, value] of Object.entries(select)) {
|
|
104
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
105
|
+
|
|
106
|
+
if (isNestedSelection(value) && value.input) {
|
|
107
|
+
inputs.set(path, value.input);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Recurse into nested selections
|
|
111
|
+
if (typeof value === "object" && value !== null) {
|
|
112
|
+
const nestedSelect = extractSelect(value);
|
|
113
|
+
if (nestedSelect) {
|
|
114
|
+
const nestedInputs = extractNestedInputs(nestedSelect, path);
|
|
115
|
+
for (const [nestedPath, nestedInput] of nestedInputs) {
|
|
116
|
+
inputs.set(nestedPath, nestedInput);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return inputs;
|
|
123
|
+
}
|
package/src/server/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
EntityDef,
|
|
10
10
|
InferRouterContext,
|
|
11
11
|
MutationDef,
|
|
12
|
+
Observable,
|
|
12
13
|
OptimisticDSL,
|
|
13
14
|
QueryDef,
|
|
14
15
|
Resolvers,
|
|
@@ -20,9 +21,26 @@ import type { PluginManager, ServerPlugin } from "../plugin/types.js";
|
|
|
20
21
|
// Selection Types
|
|
21
22
|
// =============================================================================
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Nested selection object with optional input.
|
|
26
|
+
* Used for relations that need their own params.
|
|
27
|
+
*/
|
|
28
|
+
export interface NestedSelection {
|
|
29
|
+
/** Input/params for this nested query */
|
|
30
|
+
input?: Record<string, unknown>;
|
|
31
|
+
/** Field selection for this nested query */
|
|
32
|
+
select?: SelectionObject;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Selection object for field selection.
|
|
37
|
+
* Supports:
|
|
38
|
+
* - `true` - Select this field
|
|
39
|
+
* - `{ select: {...} }` - Nested selection only
|
|
40
|
+
* - `{ input: {...}, select?: {...} }` - Nested with input params
|
|
41
|
+
*/
|
|
24
42
|
export interface SelectionObject {
|
|
25
|
-
[key: string]: boolean | SelectionObject | { select: SelectionObject };
|
|
43
|
+
[key: string]: boolean | SelectionObject | { select: SelectionObject } | NestedSelection;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
// =============================================================================
|
|
@@ -164,8 +182,20 @@ export interface WebSocketLike {
|
|
|
164
182
|
export interface LensServer {
|
|
165
183
|
/** Get server metadata for transport handshake */
|
|
166
184
|
getMetadata(): ServerMetadata;
|
|
167
|
-
|
|
168
|
-
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Execute operation - auto-detects query vs mutation.
|
|
188
|
+
*
|
|
189
|
+
* Always returns Observable<LensResult>:
|
|
190
|
+
* - One-shot: emits once, then completes
|
|
191
|
+
* - Streaming: emits multiple times (AsyncIterable or emit-based)
|
|
192
|
+
*
|
|
193
|
+
* Usage:
|
|
194
|
+
* - HTTP: `await firstValueFrom(server.execute(op))`
|
|
195
|
+
* - WS/SSE: `server.execute(op).subscribe(...)`
|
|
196
|
+
* - direct: pass through Observable directly
|
|
197
|
+
*/
|
|
198
|
+
execute(op: LensOperation): Observable<LensResult>;
|
|
169
199
|
|
|
170
200
|
// =========================================================================
|
|
171
201
|
// Subscription Support (Optional - used by WS/SSE handlers)
|