@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/dist/index.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ declare function extendContext<
|
|
|
41
41
|
E extends ContextValue
|
|
42
42
|
>(current: T, extension: E): T & E;
|
|
43
43
|
import { ContextValue as ContextValue3, InferRouterContext as InferRouterContext2, RouterDef as RouterDef2 } from "@sylphx/lens-core";
|
|
44
|
-
import { ContextValue as ContextValue2, EntityDef, InferRouterContext, MutationDef, OptimisticDSL, QueryDef, Resolvers, RouterDef } from "@sylphx/lens-core";
|
|
44
|
+
import { ContextValue as ContextValue2, EntityDef, InferRouterContext, MutationDef, Observable, OptimisticDSL, QueryDef, Resolvers, RouterDef } from "@sylphx/lens-core";
|
|
45
45
|
/**
|
|
46
46
|
* @sylphx/lens-server - Plugin System Types
|
|
47
47
|
*
|
|
@@ -436,11 +436,27 @@ declare class PluginManager {
|
|
|
436
436
|
*/
|
|
437
437
|
declare function createPluginManager(): PluginManager;
|
|
438
438
|
import { FieldType } from "@sylphx/lens-core";
|
|
439
|
-
/**
|
|
439
|
+
/**
|
|
440
|
+
* Nested selection object with optional input.
|
|
441
|
+
* Used for relations that need their own params.
|
|
442
|
+
*/
|
|
443
|
+
interface NestedSelection {
|
|
444
|
+
/** Input/params for this nested query */
|
|
445
|
+
input?: Record<string, unknown>;
|
|
446
|
+
/** Field selection for this nested query */
|
|
447
|
+
select?: SelectionObject;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Selection object for field selection.
|
|
451
|
+
* Supports:
|
|
452
|
+
* - `true` - Select this field
|
|
453
|
+
* - `{ select: {...} }` - Nested selection only
|
|
454
|
+
* - `{ input: {...}, select?: {...} }` - Nested with input params
|
|
455
|
+
*/
|
|
440
456
|
interface SelectionObject {
|
|
441
457
|
[key: string]: boolean | SelectionObject | {
|
|
442
458
|
select: SelectionObject;
|
|
443
|
-
};
|
|
459
|
+
} | NestedSelection;
|
|
444
460
|
}
|
|
445
461
|
/** Entity map type */
|
|
446
462
|
type EntitiesMap = Record<string, EntityDef<string, any>>;
|
|
@@ -539,8 +555,19 @@ interface WebSocketLike {
|
|
|
539
555
|
interface LensServer {
|
|
540
556
|
/** Get server metadata for transport handshake */
|
|
541
557
|
getMetadata(): ServerMetadata;
|
|
542
|
-
/**
|
|
543
|
-
|
|
558
|
+
/**
|
|
559
|
+
* Execute operation - auto-detects query vs mutation.
|
|
560
|
+
*
|
|
561
|
+
* Always returns Observable<LensResult>:
|
|
562
|
+
* - One-shot: emits once, then completes
|
|
563
|
+
* - Streaming: emits multiple times (AsyncIterable or emit-based)
|
|
564
|
+
*
|
|
565
|
+
* Usage:
|
|
566
|
+
* - HTTP: `await firstValueFrom(server.execute(op))`
|
|
567
|
+
* - WS/SSE: `server.execute(op).subscribe(...)`
|
|
568
|
+
* - direct: pass through Observable directly
|
|
569
|
+
*/
|
|
570
|
+
execute(op: LensOperation): Observable<LensResult>;
|
|
544
571
|
/**
|
|
545
572
|
* Register a client connection.
|
|
546
573
|
* Call when a client connects via WebSocket/SSE.
|
package/dist/index.js
CHANGED
|
@@ -215,6 +215,20 @@ class DataLoader {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
// src/server/selection.ts
|
|
218
|
+
function extractSelect(value) {
|
|
219
|
+
if (value === true)
|
|
220
|
+
return null;
|
|
221
|
+
if (typeof value !== "object" || value === null)
|
|
222
|
+
return null;
|
|
223
|
+
const obj = value;
|
|
224
|
+
if ("input" in obj || "select" in obj && typeof obj.select === "object") {
|
|
225
|
+
return obj.select ?? null;
|
|
226
|
+
}
|
|
227
|
+
if ("select" in obj && typeof obj.select === "object") {
|
|
228
|
+
return obj.select;
|
|
229
|
+
}
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
218
232
|
function applySelection(data, select) {
|
|
219
233
|
if (!data)
|
|
220
234
|
return data;
|
|
@@ -233,8 +247,12 @@ function applySelection(data, select) {
|
|
|
233
247
|
if (value === true) {
|
|
234
248
|
result[key] = obj[key];
|
|
235
249
|
} else if (typeof value === "object" && value !== null) {
|
|
236
|
-
const nestedSelect =
|
|
237
|
-
|
|
250
|
+
const nestedSelect = extractSelect(value);
|
|
251
|
+
if (nestedSelect) {
|
|
252
|
+
result[key] = applySelection(obj[key], nestedSelect);
|
|
253
|
+
} else {
|
|
254
|
+
result[key] = obj[key];
|
|
255
|
+
}
|
|
238
256
|
}
|
|
239
257
|
}
|
|
240
258
|
return result;
|
|
@@ -319,90 +337,167 @@ class LensServerImpl {
|
|
|
319
337
|
operations: this.buildOperationsMap()
|
|
320
338
|
};
|
|
321
339
|
}
|
|
322
|
-
|
|
340
|
+
execute(op) {
|
|
323
341
|
const { path, input } = op;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
} catch (error) {
|
|
335
|
-
return { error: error instanceof Error ? error : new Error(String(error)) };
|
|
342
|
+
const isQuery = !!this.queries[path];
|
|
343
|
+
const isMutation = !!this.mutations[path];
|
|
344
|
+
if (!isQuery && !isMutation) {
|
|
345
|
+
return {
|
|
346
|
+
subscribe: (observer) => {
|
|
347
|
+
observer.next?.({ error: new Error(`Operation not found: ${path}`) });
|
|
348
|
+
observer.complete?.();
|
|
349
|
+
return { unsubscribe: () => {} };
|
|
350
|
+
}
|
|
351
|
+
};
|
|
336
352
|
}
|
|
353
|
+
return this.executeAsObservable(path, input, isQuery);
|
|
337
354
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
355
|
+
executeAsObservable(path, input, isQuery) {
|
|
356
|
+
return {
|
|
357
|
+
subscribe: (observer) => {
|
|
358
|
+
let cancelled = false;
|
|
359
|
+
let currentState;
|
|
360
|
+
const cleanups = [];
|
|
361
|
+
(async () => {
|
|
362
|
+
try {
|
|
363
|
+
const def = isQuery ? this.queries[path] : this.mutations[path];
|
|
364
|
+
if (!def) {
|
|
365
|
+
observer.next?.({ error: new Error(`Operation not found: ${path}`) });
|
|
366
|
+
observer.complete?.();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
let select;
|
|
370
|
+
let cleanInput = input;
|
|
371
|
+
if (isQuery && input && typeof input === "object" && "$select" in input) {
|
|
372
|
+
const { $select, ...rest } = input;
|
|
373
|
+
select = $select;
|
|
374
|
+
cleanInput = Object.keys(rest).length > 0 ? rest : undefined;
|
|
375
|
+
}
|
|
376
|
+
if (def._input && cleanInput !== undefined) {
|
|
377
|
+
const result = def._input.safeParse(cleanInput);
|
|
378
|
+
if (!result.success) {
|
|
379
|
+
observer.next?.({
|
|
380
|
+
error: new Error(`Invalid input: ${JSON.stringify(result.error)}`)
|
|
381
|
+
});
|
|
382
|
+
observer.complete?.();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const context = await this.contextFactory();
|
|
387
|
+
await runWithContext(this.ctx, context, async () => {
|
|
388
|
+
const resolver = def._resolve;
|
|
389
|
+
if (!resolver) {
|
|
390
|
+
observer.next?.({ error: new Error(`Operation ${path} has no resolver`) });
|
|
391
|
+
observer.complete?.();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const emitHandler = (command) => {
|
|
395
|
+
if (cancelled)
|
|
396
|
+
return;
|
|
397
|
+
currentState = this.applyEmitCommand(command, currentState);
|
|
398
|
+
observer.next?.({ data: currentState });
|
|
399
|
+
};
|
|
400
|
+
const emit = createEmit(emitHandler);
|
|
401
|
+
const onCleanup = (fn) => {
|
|
402
|
+
cleanups.push(fn);
|
|
403
|
+
return () => {
|
|
404
|
+
const idx = cleanups.indexOf(fn);
|
|
405
|
+
if (idx >= 0)
|
|
406
|
+
cleanups.splice(idx, 1);
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
const lensContext = { ...context, emit, onCleanup };
|
|
410
|
+
const result = resolver({ input: cleanInput, ctx: lensContext });
|
|
411
|
+
if (isAsyncIterable(result)) {
|
|
412
|
+
for await (const value of result) {
|
|
413
|
+
if (cancelled)
|
|
414
|
+
break;
|
|
415
|
+
currentState = value;
|
|
416
|
+
const processed = await this.processQueryResult(path, value, select);
|
|
417
|
+
observer.next?.({ data: processed });
|
|
418
|
+
}
|
|
419
|
+
if (!cancelled) {
|
|
420
|
+
observer.complete?.();
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
const value = await result;
|
|
424
|
+
currentState = value;
|
|
425
|
+
const processed = isQuery ? await this.processQueryResult(path, value, select) : value;
|
|
426
|
+
if (!cancelled) {
|
|
427
|
+
observer.next?.({ data: processed });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (!cancelled) {
|
|
433
|
+
observer.next?.({ error: error instanceof Error ? error : new Error(String(error)) });
|
|
434
|
+
observer.complete?.();
|
|
435
|
+
}
|
|
436
|
+
} finally {
|
|
437
|
+
this.clearLoaders();
|
|
370
438
|
}
|
|
371
|
-
|
|
372
|
-
|
|
439
|
+
})();
|
|
440
|
+
return {
|
|
441
|
+
unsubscribe: () => {
|
|
442
|
+
cancelled = true;
|
|
443
|
+
for (const fn of cleanups) {
|
|
444
|
+
fn();
|
|
445
|
+
}
|
|
373
446
|
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return this.processQueryResult(name, data, select);
|
|
378
|
-
});
|
|
379
|
-
} finally {
|
|
380
|
-
this.clearLoaders();
|
|
381
|
-
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
};
|
|
382
450
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
451
|
+
applyEmitCommand(command, state) {
|
|
452
|
+
switch (command.type) {
|
|
453
|
+
case "full":
|
|
454
|
+
if (command.replace) {
|
|
455
|
+
return command.data;
|
|
456
|
+
}
|
|
457
|
+
if (state && typeof state === "object" && typeof command.data === "object") {
|
|
458
|
+
return { ...state, ...command.data };
|
|
459
|
+
}
|
|
460
|
+
return command.data;
|
|
461
|
+
case "field":
|
|
462
|
+
if (state && typeof state === "object") {
|
|
463
|
+
return {
|
|
464
|
+
...state,
|
|
465
|
+
[command.field]: command.update.data
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return { [command.field]: command.update.data };
|
|
469
|
+
case "batch":
|
|
470
|
+
if (state && typeof state === "object") {
|
|
471
|
+
const result = { ...state };
|
|
472
|
+
for (const update of command.updates) {
|
|
473
|
+
result[update.field] = update.update.data;
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
return state;
|
|
478
|
+
case "array": {
|
|
479
|
+
const arr = Array.isArray(state) ? [...state] : [];
|
|
480
|
+
const op = command.operation;
|
|
481
|
+
switch (op.op) {
|
|
482
|
+
case "push":
|
|
483
|
+
return [...arr, op.item];
|
|
484
|
+
case "unshift":
|
|
485
|
+
return [op.item, ...arr];
|
|
486
|
+
case "insert":
|
|
487
|
+
arr.splice(op.index, 0, op.item);
|
|
488
|
+
return arr;
|
|
489
|
+
case "remove":
|
|
490
|
+
arr.splice(op.index, 1);
|
|
491
|
+
return arr;
|
|
492
|
+
case "update":
|
|
493
|
+
arr[op.index] = op.item;
|
|
494
|
+
return arr;
|
|
495
|
+
default:
|
|
496
|
+
return arr;
|
|
497
|
+
}
|
|
391
498
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
return await runWithContext(this.ctx, context, async () => {
|
|
396
|
-
const resolver = mutationDef._resolve;
|
|
397
|
-
if (!resolver)
|
|
398
|
-
throw new Error(`Mutation ${name} has no resolver`);
|
|
399
|
-
const emit = createEmit(() => {});
|
|
400
|
-
const onCleanup = () => () => {};
|
|
401
|
-
const lensContext = { ...context, emit, onCleanup };
|
|
402
|
-
return await resolver({ input, ctx: lensContext });
|
|
403
|
-
});
|
|
404
|
-
} finally {
|
|
405
|
-
this.clearLoaders();
|
|
499
|
+
default:
|
|
500
|
+
return state;
|
|
406
501
|
}
|
|
407
502
|
}
|
|
408
503
|
async processQueryResult(_operationName, data, select) {
|
|
@@ -698,6 +793,7 @@ function createSSEHandler(config = {}) {
|
|
|
698
793
|
}
|
|
699
794
|
|
|
700
795
|
// src/handlers/http.ts
|
|
796
|
+
import { firstValueFrom } from "@sylphx/lens-core";
|
|
701
797
|
function createHTTPHandler(server, options = {}) {
|
|
702
798
|
const { pathPrefix = "", cors } = options;
|
|
703
799
|
const corsHeaders = {
|
|
@@ -737,10 +833,10 @@ function createHTTPHandler(server, options = {}) {
|
|
|
737
833
|
}
|
|
738
834
|
});
|
|
739
835
|
}
|
|
740
|
-
const result2 = await server.execute({
|
|
836
|
+
const result2 = await firstValueFrom(server.execute({
|
|
741
837
|
path: operationPath2,
|
|
742
838
|
input: body.input
|
|
743
|
-
});
|
|
839
|
+
}));
|
|
744
840
|
if (result2.error) {
|
|
745
841
|
return new Response(JSON.stringify({ error: result2.error.message }), {
|
|
746
842
|
status: 500,
|
|
@@ -808,6 +904,7 @@ function createHandler(server, options = {}) {
|
|
|
808
904
|
return result;
|
|
809
905
|
}
|
|
810
906
|
// src/handlers/framework.ts
|
|
907
|
+
import { firstValueFrom as firstValueFrom2 } from "@sylphx/lens-core";
|
|
811
908
|
function createServerClientProxy(server) {
|
|
812
909
|
function createProxy(path) {
|
|
813
910
|
return new Proxy(() => {}, {
|
|
@@ -821,7 +918,7 @@ function createServerClientProxy(server) {
|
|
|
821
918
|
},
|
|
822
919
|
async apply(_, __, args) {
|
|
823
920
|
const input = args[0];
|
|
824
|
-
const result = await server.execute({ path, input });
|
|
921
|
+
const result = await firstValueFrom2(server.execute({ path, input }));
|
|
825
922
|
if (result.error) {
|
|
826
923
|
throw result.error;
|
|
827
924
|
}
|
|
@@ -835,7 +932,7 @@ async function handleWebQuery(server, path, url) {
|
|
|
835
932
|
try {
|
|
836
933
|
const inputParam = url.searchParams.get("input");
|
|
837
934
|
const input = inputParam ? JSON.parse(inputParam) : undefined;
|
|
838
|
-
const result = await server.execute({ path, input });
|
|
935
|
+
const result = await firstValueFrom2(server.execute({ path, input }));
|
|
839
936
|
if (result.error) {
|
|
840
937
|
return Response.json({ error: result.error.message }, { status: 400 });
|
|
841
938
|
}
|
|
@@ -848,7 +945,7 @@ async function handleWebMutation(server, path, request) {
|
|
|
848
945
|
try {
|
|
849
946
|
const body = await request.json();
|
|
850
947
|
const input = body.input;
|
|
851
|
-
const result = await server.execute({ path, input });
|
|
948
|
+
const result = await firstValueFrom2(server.execute({ path, input }));
|
|
852
949
|
if (result.error) {
|
|
853
950
|
return Response.json({ error: result.error.message }, { status: 400 });
|
|
854
951
|
}
|
|
@@ -920,6 +1017,9 @@ function createFrameworkHandler(server, options = {}) {
|
|
|
920
1017
|
};
|
|
921
1018
|
}
|
|
922
1019
|
// src/handlers/ws.ts
|
|
1020
|
+
import {
|
|
1021
|
+
firstValueFrom as firstValueFrom3
|
|
1022
|
+
} from "@sylphx/lens-core";
|
|
923
1023
|
function createWSHandler(server, options = {}) {
|
|
924
1024
|
const { logger = {} } = options;
|
|
925
1025
|
const connections = new Map;
|
|
@@ -996,7 +1096,7 @@ function createWSHandler(server, options = {}) {
|
|
|
996
1096
|
const { id, operation, input, fields } = message;
|
|
997
1097
|
let result;
|
|
998
1098
|
try {
|
|
999
|
-
result = await server.execute({ path: operation, input });
|
|
1099
|
+
result = await firstValueFrom3(server.execute({ path: operation, input }));
|
|
1000
1100
|
if (result.error) {
|
|
1001
1101
|
conn.ws.send(JSON.stringify({
|
|
1002
1102
|
type: "error",
|
|
@@ -1122,10 +1222,10 @@ function createWSHandler(server, options = {}) {
|
|
|
1122
1222
|
}
|
|
1123
1223
|
async function handleQuery(conn, message) {
|
|
1124
1224
|
try {
|
|
1125
|
-
const result = await server.execute({
|
|
1225
|
+
const result = await firstValueFrom3(server.execute({
|
|
1126
1226
|
path: message.operation,
|
|
1127
1227
|
input: message.input
|
|
1128
|
-
});
|
|
1228
|
+
}));
|
|
1129
1229
|
if (result.error) {
|
|
1130
1230
|
conn.ws.send(JSON.stringify({
|
|
1131
1231
|
type: "error",
|
|
@@ -1150,10 +1250,10 @@ function createWSHandler(server, options = {}) {
|
|
|
1150
1250
|
}
|
|
1151
1251
|
async function handleMutation(conn, message) {
|
|
1152
1252
|
try {
|
|
1153
|
-
const result = await server.execute({
|
|
1253
|
+
const result = await firstValueFrom3(server.execute({
|
|
1154
1254
|
path: message.operation,
|
|
1155
1255
|
input: message.input
|
|
1156
|
-
});
|
|
1256
|
+
}));
|
|
1157
1257
|
if (result.error) {
|
|
1158
1258
|
conn.ws.send(JSON.stringify({
|
|
1159
1259
|
type: "error",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/lens-server",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
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.0
|
|
33
|
+
"@sylphx/lens-core": "^2.1.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.9.3",
|
package/src/e2e/server.test.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it } from "bun:test";
|
|
11
|
-
import { entity, lens, mutation, query, t } from "@sylphx/lens-core";
|
|
11
|
+
import { entity, firstValueFrom, lens, mutation, query, t } from "@sylphx/lens-core";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { optimisticPlugin } from "../plugin/optimistic.js";
|
|
14
14
|
import { createApp } from "../server/create.js";
|
|
@@ -58,7 +58,7 @@ describe("E2E - Basic Operations", () => {
|
|
|
58
58
|
queries: { getUsers },
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
const result = await server.execute({ path: "getUsers" });
|
|
61
|
+
const result = await firstValueFrom(server.execute({ path: "getUsers" }));
|
|
62
62
|
|
|
63
63
|
expect(result.error).toBeUndefined();
|
|
64
64
|
expect(result.data).toEqual(mockUsers);
|
|
@@ -79,10 +79,12 @@ describe("E2E - Basic Operations", () => {
|
|
|
79
79
|
queries: { getUser },
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
const result = await
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
const result = await firstValueFrom(
|
|
83
|
+
server.execute({
|
|
84
|
+
path: "getUser",
|
|
85
|
+
input: { id: "user-1" },
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
86
88
|
|
|
87
89
|
expect(result.error).toBeUndefined();
|
|
88
90
|
expect(result.data).toEqual(mockUsers[0]);
|
|
@@ -104,10 +106,12 @@ describe("E2E - Basic Operations", () => {
|
|
|
104
106
|
mutations: { createUser },
|
|
105
107
|
});
|
|
106
108
|
|
|
107
|
-
const result = await
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
const result = await firstValueFrom(
|
|
110
|
+
server.execute({
|
|
111
|
+
path: "createUser",
|
|
112
|
+
input: { name: "Charlie", email: "charlie@example.com" },
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
111
115
|
|
|
112
116
|
expect(result.error).toBeUndefined();
|
|
113
117
|
expect(result.data).toEqual({
|
|
@@ -129,10 +133,12 @@ describe("E2E - Basic Operations", () => {
|
|
|
129
133
|
queries: { failingQuery },
|
|
130
134
|
});
|
|
131
135
|
|
|
132
|
-
const result = await
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
const result = await firstValueFrom(
|
|
137
|
+
server.execute({
|
|
138
|
+
path: "failingQuery",
|
|
139
|
+
input: { id: "123" },
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
136
142
|
|
|
137
143
|
expect(result.data).toBeUndefined();
|
|
138
144
|
expect(result.error).toBeInstanceOf(Error);
|
|
@@ -142,10 +148,12 @@ describe("E2E - Basic Operations", () => {
|
|
|
142
148
|
it("handles unknown operation", async () => {
|
|
143
149
|
const server = createApp({});
|
|
144
150
|
|
|
145
|
-
const result = await
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
const result = await firstValueFrom(
|
|
152
|
+
server.execute({
|
|
153
|
+
path: "unknownOperation",
|
|
154
|
+
input: {},
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
149
157
|
|
|
150
158
|
expect(result.data).toBeUndefined();
|
|
151
159
|
expect(result.error?.message).toContain("not found");
|
|
@@ -172,10 +180,12 @@ describe("E2E - Context", () => {
|
|
|
172
180
|
context: () => ({ userId: "ctx-user-1", role: "admin" }),
|
|
173
181
|
});
|
|
174
182
|
|
|
175
|
-
await
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
await firstValueFrom(
|
|
184
|
+
server.execute({
|
|
185
|
+
path: "getUser",
|
|
186
|
+
input: { id: "user-1" },
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
179
189
|
|
|
180
190
|
expect(capturedContext).toMatchObject({
|
|
181
191
|
userId: "ctx-user-1",
|
|
@@ -201,10 +211,12 @@ describe("E2E - Context", () => {
|
|
|
201
211
|
},
|
|
202
212
|
});
|
|
203
213
|
|
|
204
|
-
await
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
214
|
+
await firstValueFrom(
|
|
215
|
+
server.execute({
|
|
216
|
+
path: "getUser",
|
|
217
|
+
input: { id: "user-1" },
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
208
220
|
|
|
209
221
|
expect(capturedContext).toMatchObject({
|
|
210
222
|
userId: "async-user",
|
|
@@ -232,13 +244,15 @@ describe("E2E - Selection", () => {
|
|
|
232
244
|
queries: { getUser },
|
|
233
245
|
});
|
|
234
246
|
|
|
235
|
-
const result = await
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
247
|
+
const result = await firstValueFrom(
|
|
248
|
+
server.execute({
|
|
249
|
+
path: "getUser",
|
|
250
|
+
input: {
|
|
251
|
+
id: "user-1",
|
|
252
|
+
$select: { name: true },
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
242
256
|
|
|
243
257
|
expect(result.error).toBeUndefined();
|
|
244
258
|
// Should include id (always) and selected fields
|
|
@@ -262,13 +276,15 @@ describe("E2E - Selection", () => {
|
|
|
262
276
|
queries: { getUser },
|
|
263
277
|
});
|
|
264
278
|
|
|
265
|
-
const result = await
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
279
|
+
const result = await firstValueFrom(
|
|
280
|
+
server.execute({
|
|
281
|
+
path: "getUser",
|
|
282
|
+
input: {
|
|
283
|
+
id: "user-1",
|
|
284
|
+
$select: { email: true },
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
272
288
|
|
|
273
289
|
expect(result.data).toEqual({
|
|
274
290
|
id: "user-1",
|
|
@@ -318,20 +334,22 @@ describe("E2E - Entity Resolvers", () => {
|
|
|
318
334
|
});
|
|
319
335
|
|
|
320
336
|
// Test with $select for nested posts
|
|
321
|
-
const result = await
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
337
|
+
const result = await firstValueFrom(
|
|
338
|
+
server.execute({
|
|
339
|
+
path: "getUser",
|
|
340
|
+
input: {
|
|
341
|
+
id: "user-1",
|
|
342
|
+
$select: {
|
|
343
|
+
name: true,
|
|
344
|
+
posts: {
|
|
345
|
+
select: {
|
|
346
|
+
title: true,
|
|
347
|
+
},
|
|
330
348
|
},
|
|
331
349
|
},
|
|
332
350
|
},
|
|
333
|
-
},
|
|
334
|
-
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
335
353
|
|
|
336
354
|
expect(result.error).toBeUndefined();
|
|
337
355
|
expect(result.data).toMatchObject({
|
|
@@ -380,19 +398,21 @@ describe("E2E - Entity Resolvers", () => {
|
|
|
380
398
|
});
|
|
381
399
|
|
|
382
400
|
// Execute query with nested selection for all users
|
|
383
|
-
const result = await
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
401
|
+
const result = await firstValueFrom(
|
|
402
|
+
server.execute({
|
|
403
|
+
path: "getUsers",
|
|
404
|
+
input: {
|
|
405
|
+
$select: {
|
|
406
|
+
name: true,
|
|
407
|
+
posts: {
|
|
408
|
+
select: {
|
|
409
|
+
title: true,
|
|
410
|
+
},
|
|
391
411
|
},
|
|
392
412
|
},
|
|
393
413
|
},
|
|
394
|
-
},
|
|
395
|
-
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
396
416
|
|
|
397
417
|
expect(result.error).toBeUndefined();
|
|
398
418
|
// Resolvers are called - exact count depends on DataLoader batching behavior
|