@sylphx/lens-server 1.3.2 → 1.5.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/README.md +35 -0
- package/dist/index.d.ts +23 -109
- package/dist/index.js +58 -38
- package/package.json +37 -36
- package/src/e2e/server.test.ts +56 -45
- package/src/index.ts +26 -29
- package/src/server/create.test.ts +997 -20
- package/src/server/create.ts +82 -85
- package/src/sse/handler.ts +1 -1
- package/src/state/graph-state-manager.test.ts +566 -10
- package/src/state/graph-state-manager.ts +38 -13
- package/src/state/index.ts +3 -3
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { describe, expect, it } from "bun:test";
|
|
8
|
-
import { entity, mutation, query, t } from "@sylphx/lens-core";
|
|
8
|
+
import { entity, mutation, query, resolver, t } from "@sylphx/lens-core";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { type WebSocketLike
|
|
10
|
+
import { createServer, type WebSocketLike } from "./create";
|
|
11
11
|
|
|
12
12
|
// =============================================================================
|
|
13
13
|
// Test Fixtures
|
|
@@ -34,7 +34,7 @@ const mockUsers = [
|
|
|
34
34
|
{ id: "user-2", name: "Bob", email: "bob@example.com", bio: "Designer" },
|
|
35
35
|
];
|
|
36
36
|
|
|
37
|
-
const
|
|
37
|
+
const _mockPosts = [
|
|
38
38
|
{ id: "post-1", title: "Hello", content: "World", authorId: "user-1" },
|
|
39
39
|
{ id: "post-2", title: "Test", content: "Post", authorId: "user-1" },
|
|
40
40
|
];
|
|
@@ -140,9 +140,7 @@ describe("executeQuery", () => {
|
|
|
140
140
|
queries: { getUser },
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
await expect(server.executeQuery("getUser", { id: 123 as unknown as string })).rejects.toThrow(
|
|
144
|
-
"Invalid input",
|
|
145
|
-
);
|
|
143
|
+
await expect(server.executeQuery("getUser", { id: 123 as unknown as string })).rejects.toThrow("Invalid input");
|
|
146
144
|
});
|
|
147
145
|
|
|
148
146
|
it("throws for unknown query", async () => {
|
|
@@ -151,9 +149,7 @@ describe("executeQuery", () => {
|
|
|
151
149
|
queries: {},
|
|
152
150
|
});
|
|
153
151
|
|
|
154
|
-
await expect(server.executeQuery("unknownQuery")).rejects.toThrow(
|
|
155
|
-
"Query not found: unknownQuery",
|
|
156
|
-
);
|
|
152
|
+
await expect(server.executeQuery("unknownQuery")).rejects.toThrow("Query not found: unknownQuery");
|
|
157
153
|
});
|
|
158
154
|
});
|
|
159
155
|
|
|
@@ -200,9 +196,9 @@ describe("executeMutation", () => {
|
|
|
200
196
|
mutations: { createUser },
|
|
201
197
|
});
|
|
202
198
|
|
|
203
|
-
await expect(
|
|
204
|
-
|
|
205
|
-
)
|
|
199
|
+
await expect(server.executeMutation("createUser", { name: "Test", email: "invalid-email" })).rejects.toThrow(
|
|
200
|
+
"Invalid input",
|
|
201
|
+
);
|
|
206
202
|
});
|
|
207
203
|
|
|
208
204
|
it("throws for unknown mutation", async () => {
|
|
@@ -211,9 +207,7 @@ describe("executeMutation", () => {
|
|
|
211
207
|
mutations: {},
|
|
212
208
|
});
|
|
213
209
|
|
|
214
|
-
await expect(server.executeMutation("unknownMutation", {})).rejects.toThrow(
|
|
215
|
-
"Mutation not found: unknownMutation",
|
|
216
|
-
);
|
|
210
|
+
await expect(server.executeMutation("unknownMutation", {})).rejects.toThrow("Mutation not found: unknownMutation");
|
|
217
211
|
});
|
|
218
212
|
});
|
|
219
213
|
|
|
@@ -355,9 +349,7 @@ describe("Subscribe Protocol", () => {
|
|
|
355
349
|
expect(ws.messages.length).toBeGreaterThan(0);
|
|
356
350
|
|
|
357
351
|
// Find the operation-level data message (has subscription id)
|
|
358
|
-
const dataMessage = ws.messages
|
|
359
|
-
.map((m) => JSON.parse(m))
|
|
360
|
-
.find((m) => m.type === "data" && m.id === "sub-1");
|
|
352
|
+
const dataMessage = ws.messages.map((m) => JSON.parse(m)).find((m) => m.type === "data" && m.id === "sub-1");
|
|
361
353
|
|
|
362
354
|
// Should have received operation-level data
|
|
363
355
|
expect(dataMessage).toBeDefined();
|
|
@@ -640,12 +632,12 @@ describe("Streaming Support", () => {
|
|
|
640
632
|
|
|
641
633
|
describe("Minimum Transfer", () => {
|
|
642
634
|
it("sends initial data on subscribe", async () => {
|
|
643
|
-
let
|
|
635
|
+
let _emitFn: ((data: unknown) => void) | null = null;
|
|
644
636
|
|
|
645
637
|
const liveQuery = query()
|
|
646
638
|
.returns(User)
|
|
647
639
|
.resolve(({ emit }) => {
|
|
648
|
-
|
|
640
|
+
_emitFn = emit;
|
|
649
641
|
return { id: "1", name: "Alice", email: "alice@example.com" };
|
|
650
642
|
});
|
|
651
643
|
|
|
@@ -804,4 +796,989 @@ describe("onCleanup", () => {
|
|
|
804
796
|
|
|
805
797
|
expect(cleanedUp).toBe(true);
|
|
806
798
|
});
|
|
799
|
+
|
|
800
|
+
it("allows cleanup removal via returned function", async () => {
|
|
801
|
+
let cleanedUp = false;
|
|
802
|
+
|
|
803
|
+
const liveQuery = query()
|
|
804
|
+
.returns(User)
|
|
805
|
+
.resolve(({ onCleanup }) => {
|
|
806
|
+
const remove = onCleanup(() => {
|
|
807
|
+
cleanedUp = true;
|
|
808
|
+
});
|
|
809
|
+
// Remove the cleanup before unsubscribe
|
|
810
|
+
remove();
|
|
811
|
+
return mockUsers[0];
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
const server = createServer({
|
|
815
|
+
entities: { User },
|
|
816
|
+
queries: { liveQuery },
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const ws = createMockWs();
|
|
820
|
+
server.handleWebSocket(ws);
|
|
821
|
+
|
|
822
|
+
// Subscribe
|
|
823
|
+
ws.onmessage?.({
|
|
824
|
+
data: JSON.stringify({
|
|
825
|
+
type: "subscribe",
|
|
826
|
+
id: "sub-1",
|
|
827
|
+
operation: "liveQuery",
|
|
828
|
+
fields: "*",
|
|
829
|
+
}),
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
833
|
+
|
|
834
|
+
// Unsubscribe
|
|
835
|
+
ws.onmessage?.({
|
|
836
|
+
data: JSON.stringify({ type: "unsubscribe", id: "sub-1" }),
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Should not have cleaned up since we removed it
|
|
840
|
+
expect(cleanedUp).toBe(false);
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// =============================================================================
|
|
845
|
+
// Test: execute() method (for in-process transport)
|
|
846
|
+
// =============================================================================
|
|
847
|
+
|
|
848
|
+
describe("execute method", () => {
|
|
849
|
+
it("executes a query operation", async () => {
|
|
850
|
+
const getUsers = query()
|
|
851
|
+
.returns([User])
|
|
852
|
+
.resolve(() => mockUsers);
|
|
853
|
+
|
|
854
|
+
const server = createServer({
|
|
855
|
+
entities: { User },
|
|
856
|
+
queries: { getUsers },
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
const result = await server.execute({ path: "getUsers" });
|
|
860
|
+
expect(result.data).toEqual(mockUsers);
|
|
861
|
+
expect(result.error).toBeUndefined();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("executes a mutation operation", async () => {
|
|
865
|
+
const createUser = mutation()
|
|
866
|
+
.input(z.object({ name: z.string() }))
|
|
867
|
+
.returns(User)
|
|
868
|
+
.resolve(({ input }) => ({ id: "new", name: input.name, email: "" }));
|
|
869
|
+
|
|
870
|
+
const server = createServer({
|
|
871
|
+
entities: { User },
|
|
872
|
+
mutations: { createUser },
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
const result = await server.execute({ path: "createUser", input: { name: "Test" } });
|
|
876
|
+
expect(result.data).toEqual({ id: "new", name: "Test", email: "" });
|
|
877
|
+
expect(result.error).toBeUndefined();
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("returns error for unknown operation", async () => {
|
|
881
|
+
const server = createServer({
|
|
882
|
+
entities: { User },
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const result = await server.execute({ path: "unknownOp" });
|
|
886
|
+
expect(result.data).toBeUndefined();
|
|
887
|
+
expect(result.error).toBeDefined();
|
|
888
|
+
expect(result.error?.message).toBe("Operation not found: unknownOp");
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("catches and returns errors from operations", async () => {
|
|
892
|
+
const errorQuery = query()
|
|
893
|
+
.returns(User)
|
|
894
|
+
.resolve(() => {
|
|
895
|
+
throw new Error("Test error");
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
const server = createServer({
|
|
899
|
+
entities: { User },
|
|
900
|
+
queries: { errorQuery },
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
const result = await server.execute({ path: "errorQuery" });
|
|
904
|
+
expect(result.data).toBeUndefined();
|
|
905
|
+
expect(result.error).toBeDefined();
|
|
906
|
+
expect(result.error?.message).toBe("Test error");
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
it("converts non-Error exceptions to Error objects", async () => {
|
|
910
|
+
const errorQuery = query()
|
|
911
|
+
.returns(User)
|
|
912
|
+
.resolve(() => {
|
|
913
|
+
throw "String error";
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
const server = createServer({
|
|
917
|
+
entities: { User },
|
|
918
|
+
queries: { errorQuery },
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
const result = await server.execute({ path: "errorQuery" });
|
|
922
|
+
expect(result.error).toBeDefined();
|
|
923
|
+
expect(result.error?.message).toBe("String error");
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// =============================================================================
|
|
928
|
+
// Test: getMetadata() and buildOperationsMap()
|
|
929
|
+
// =============================================================================
|
|
930
|
+
|
|
931
|
+
describe("getMetadata", () => {
|
|
932
|
+
it("returns server metadata with version and operations", () => {
|
|
933
|
+
const getUser = query()
|
|
934
|
+
.returns(User)
|
|
935
|
+
.resolve(() => mockUsers[0]);
|
|
936
|
+
|
|
937
|
+
const createUser = mutation()
|
|
938
|
+
.input(z.object({ name: z.string() }))
|
|
939
|
+
.returns(User)
|
|
940
|
+
.resolve(({ input }) => ({ id: "new", name: input.name, email: "" }));
|
|
941
|
+
|
|
942
|
+
const server = createServer({
|
|
943
|
+
entities: { User },
|
|
944
|
+
queries: { getUser },
|
|
945
|
+
mutations: { createUser },
|
|
946
|
+
version: "1.2.3",
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
const metadata = server.getMetadata();
|
|
950
|
+
expect(metadata.version).toBe("1.2.3");
|
|
951
|
+
expect(metadata.operations).toBeDefined();
|
|
952
|
+
expect(metadata.operations.getUser).toEqual({ type: "query" });
|
|
953
|
+
// createUser auto-derives optimistic "create" from naming convention
|
|
954
|
+
expect(metadata.operations.createUser).toEqual({ type: "mutation", optimistic: "create" });
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it("builds nested operations map from namespaced routes", () => {
|
|
958
|
+
const getUserQuery = query()
|
|
959
|
+
.returns(User)
|
|
960
|
+
.resolve(() => mockUsers[0]);
|
|
961
|
+
|
|
962
|
+
const createUserMutation = mutation()
|
|
963
|
+
.input(z.object({ name: z.string() }))
|
|
964
|
+
.returns(User)
|
|
965
|
+
.resolve(({ input }) => ({ id: "new", name: input.name, email: "" }));
|
|
966
|
+
|
|
967
|
+
const server = createServer({
|
|
968
|
+
entities: { User },
|
|
969
|
+
queries: { "user.get": getUserQuery },
|
|
970
|
+
mutations: { "user.create": createUserMutation },
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
const metadata = server.getMetadata();
|
|
974
|
+
expect(metadata.operations.user).toBeDefined();
|
|
975
|
+
expect((metadata.operations.user as any).get).toEqual({ type: "query" });
|
|
976
|
+
// Auto-derives optimistic "create" from naming convention
|
|
977
|
+
expect((metadata.operations.user as any).create).toEqual({ type: "mutation", optimistic: "create" });
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("includes optimistic config in mutation metadata", () => {
|
|
981
|
+
const updateUser = mutation()
|
|
982
|
+
.input(z.object({ id: z.string(), name: z.string() }))
|
|
983
|
+
.returns(User)
|
|
984
|
+
.optimistic("merge")
|
|
985
|
+
.resolve(({ input }) => ({ id: input.id, name: input.name, email: "" }));
|
|
986
|
+
|
|
987
|
+
const server = createServer({
|
|
988
|
+
entities: { User },
|
|
989
|
+
mutations: { updateUser },
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
const metadata = server.getMetadata();
|
|
993
|
+
expect(metadata.operations.updateUser).toEqual({
|
|
994
|
+
type: "mutation",
|
|
995
|
+
optimistic: "merge",
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it("handles deeply nested namespaced operations", () => {
|
|
1000
|
+
const deepQuery = query()
|
|
1001
|
+
.returns(User)
|
|
1002
|
+
.resolve(() => mockUsers[0]);
|
|
1003
|
+
|
|
1004
|
+
const server = createServer({
|
|
1005
|
+
entities: { User },
|
|
1006
|
+
queries: { "api.v1.user.get": deepQuery },
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
const metadata = server.getMetadata();
|
|
1010
|
+
const operations = metadata.operations as any;
|
|
1011
|
+
expect(operations.api.v1.user.get).toEqual({ type: "query" });
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// =============================================================================
|
|
1016
|
+
// Test: HTTP handleRequest edge cases
|
|
1017
|
+
// =============================================================================
|
|
1018
|
+
|
|
1019
|
+
describe("handleRequest edge cases", () => {
|
|
1020
|
+
it("returns metadata on GET /__lens/metadata", async () => {
|
|
1021
|
+
const server = createServer({
|
|
1022
|
+
entities: { User },
|
|
1023
|
+
version: "1.0.0",
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
const request = new Request("http://localhost/__lens/metadata", { method: "GET" });
|
|
1027
|
+
const response = await server.handleRequest(request);
|
|
1028
|
+
|
|
1029
|
+
expect(response.status).toBe(200);
|
|
1030
|
+
const body = await response.json();
|
|
1031
|
+
expect(body.version).toBe("1.0.0");
|
|
1032
|
+
expect(body.operations).toBeDefined();
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
it("returns 404 for unknown operation", async () => {
|
|
1036
|
+
const server = createServer({
|
|
1037
|
+
entities: { User },
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
const request = new Request("http://localhost/api", {
|
|
1041
|
+
method: "POST",
|
|
1042
|
+
headers: { "Content-Type": "application/json" },
|
|
1043
|
+
body: JSON.stringify({ operation: "unknownOp" }),
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
const response = await server.handleRequest(request);
|
|
1047
|
+
expect(response.status).toBe(404);
|
|
1048
|
+
|
|
1049
|
+
const body = await response.json();
|
|
1050
|
+
expect(body.error).toBe("Operation not found: unknownOp");
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it("returns 500 for operation errors", async () => {
|
|
1054
|
+
const errorQuery = query()
|
|
1055
|
+
.returns(User)
|
|
1056
|
+
.resolve(() => {
|
|
1057
|
+
throw new Error("Internal error");
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
const server = createServer({
|
|
1061
|
+
entities: { User },
|
|
1062
|
+
queries: { errorQuery },
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
const request = new Request("http://localhost/api", {
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
headers: { "Content-Type": "application/json" },
|
|
1068
|
+
body: JSON.stringify({ operation: "errorQuery" }),
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const response = await server.handleRequest(request);
|
|
1072
|
+
expect(response.status).toBe(500);
|
|
1073
|
+
|
|
1074
|
+
const body = await response.json();
|
|
1075
|
+
expect(body.error).toContain("Internal error");
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it("handles POST requests for queries", async () => {
|
|
1079
|
+
const getUser = query()
|
|
1080
|
+
.input(z.object({ id: z.string() }))
|
|
1081
|
+
.returns(User)
|
|
1082
|
+
.resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
|
|
1083
|
+
|
|
1084
|
+
const server = createServer({
|
|
1085
|
+
entities: { User },
|
|
1086
|
+
queries: { getUser },
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const request = new Request("http://localhost/api", {
|
|
1090
|
+
method: "POST",
|
|
1091
|
+
headers: { "Content-Type": "application/json" },
|
|
1092
|
+
body: JSON.stringify({ operation: "getUser", input: { id: "user-1" } }),
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
const response = await server.handleRequest(request);
|
|
1096
|
+
expect(response.status).toBe(200);
|
|
1097
|
+
|
|
1098
|
+
const body = await response.json();
|
|
1099
|
+
expect(body.data).toEqual(mockUsers[0]);
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// =============================================================================
|
|
1104
|
+
// Test: Context creation errors
|
|
1105
|
+
// =============================================================================
|
|
1106
|
+
|
|
1107
|
+
describe("Context creation errors", () => {
|
|
1108
|
+
it("handles context factory errors in executeQuery", async () => {
|
|
1109
|
+
const getUser = query()
|
|
1110
|
+
.returns(User)
|
|
1111
|
+
.resolve(() => mockUsers[0]);
|
|
1112
|
+
|
|
1113
|
+
const server = createServer({
|
|
1114
|
+
entities: { User },
|
|
1115
|
+
queries: { getUser },
|
|
1116
|
+
context: () => {
|
|
1117
|
+
throw new Error("Context creation failed");
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
await expect(server.executeQuery("getUser")).rejects.toThrow("Context creation failed");
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it("handles async context factory errors in executeMutation", async () => {
|
|
1125
|
+
const createUser = mutation()
|
|
1126
|
+
.input(z.object({ name: z.string() }))
|
|
1127
|
+
.returns(User)
|
|
1128
|
+
.resolve(({ input }) => ({ id: "new", name: input.name, email: "" }));
|
|
1129
|
+
|
|
1130
|
+
const server = createServer({
|
|
1131
|
+
entities: { User },
|
|
1132
|
+
mutations: { createUser },
|
|
1133
|
+
context: async () => {
|
|
1134
|
+
throw new Error("Async context error");
|
|
1135
|
+
},
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
await expect(server.executeMutation("createUser", { name: "Test" })).rejects.toThrow("Async context error");
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it("handles context errors in subscription", async () => {
|
|
1142
|
+
const liveQuery = query()
|
|
1143
|
+
.returns(User)
|
|
1144
|
+
.resolve(() => mockUsers[0]);
|
|
1145
|
+
|
|
1146
|
+
const server = createServer({
|
|
1147
|
+
entities: { User },
|
|
1148
|
+
queries: { liveQuery },
|
|
1149
|
+
context: () => {
|
|
1150
|
+
throw new Error("Context error in subscription");
|
|
1151
|
+
},
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
const ws = createMockWs();
|
|
1155
|
+
server.handleWebSocket(ws);
|
|
1156
|
+
|
|
1157
|
+
ws.onmessage?.({
|
|
1158
|
+
data: JSON.stringify({
|
|
1159
|
+
type: "subscribe",
|
|
1160
|
+
id: "sub-1",
|
|
1161
|
+
operation: "liveQuery",
|
|
1162
|
+
fields: "*",
|
|
1163
|
+
}),
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1167
|
+
|
|
1168
|
+
// Should receive error message
|
|
1169
|
+
const errorMsg = ws.messages.find((m) => {
|
|
1170
|
+
const parsed = JSON.parse(m);
|
|
1171
|
+
return parsed.type === "error" && parsed.id === "sub-1";
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
expect(errorMsg).toBeDefined();
|
|
1175
|
+
const parsed = JSON.parse(errorMsg!);
|
|
1176
|
+
expect(parsed.error.message).toContain("Context error in subscription");
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// =============================================================================
|
|
1181
|
+
// Test: Subscription edge cases
|
|
1182
|
+
// =============================================================================
|
|
1183
|
+
|
|
1184
|
+
describe("Subscription edge cases", () => {
|
|
1185
|
+
it("handles subscription input validation errors", async () => {
|
|
1186
|
+
const getUser = query()
|
|
1187
|
+
.input(z.object({ id: z.string().min(5) }))
|
|
1188
|
+
.returns(User)
|
|
1189
|
+
.resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
|
|
1190
|
+
|
|
1191
|
+
const server = createServer({
|
|
1192
|
+
entities: { User },
|
|
1193
|
+
queries: { getUser },
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
const ws = createMockWs();
|
|
1197
|
+
server.handleWebSocket(ws);
|
|
1198
|
+
|
|
1199
|
+
ws.onmessage?.({
|
|
1200
|
+
data: JSON.stringify({
|
|
1201
|
+
type: "subscribe",
|
|
1202
|
+
id: "sub-1",
|
|
1203
|
+
operation: "getUser",
|
|
1204
|
+
input: { id: "a" }, // Too short
|
|
1205
|
+
fields: "*",
|
|
1206
|
+
}),
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1210
|
+
|
|
1211
|
+
const errorMsg = ws.messages.find((m) => {
|
|
1212
|
+
const parsed = JSON.parse(m);
|
|
1213
|
+
return parsed.type === "error";
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
expect(errorMsg).toBeDefined();
|
|
1217
|
+
const parsed = JSON.parse(errorMsg!);
|
|
1218
|
+
expect(parsed.error.message).toContain("Invalid input");
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it("handles updateFields for non-existent subscription", () => {
|
|
1222
|
+
const server = createServer({
|
|
1223
|
+
entities: { User },
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
const ws = createMockWs();
|
|
1227
|
+
server.handleWebSocket(ws);
|
|
1228
|
+
|
|
1229
|
+
// Try to update fields for non-existent subscription
|
|
1230
|
+
ws.onmessage?.({
|
|
1231
|
+
data: JSON.stringify({
|
|
1232
|
+
type: "updateFields",
|
|
1233
|
+
id: "non-existent",
|
|
1234
|
+
addFields: ["name"],
|
|
1235
|
+
}),
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
// Should not throw - just be a no-op
|
|
1239
|
+
expect(true).toBe(true);
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
it("handles unsubscribe for non-existent subscription", () => {
|
|
1243
|
+
const server = createServer({
|
|
1244
|
+
entities: { User },
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
const ws = createMockWs();
|
|
1248
|
+
server.handleWebSocket(ws);
|
|
1249
|
+
|
|
1250
|
+
// Try to unsubscribe from non-existent subscription
|
|
1251
|
+
ws.onmessage?.({
|
|
1252
|
+
data: JSON.stringify({
|
|
1253
|
+
type: "unsubscribe",
|
|
1254
|
+
id: "non-existent",
|
|
1255
|
+
}),
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
// Should not throw - just be a no-op
|
|
1259
|
+
expect(true).toBe(true);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
it("upgrades to full subscription with wildcard", async () => {
|
|
1263
|
+
const getUser = query()
|
|
1264
|
+
.input(z.object({ id: z.string() }))
|
|
1265
|
+
.returns(User)
|
|
1266
|
+
.resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
|
|
1267
|
+
|
|
1268
|
+
const server = createServer({
|
|
1269
|
+
entities: { User },
|
|
1270
|
+
queries: { getUser },
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
const ws = createMockWs();
|
|
1274
|
+
server.handleWebSocket(ws);
|
|
1275
|
+
|
|
1276
|
+
// Subscribe with partial fields
|
|
1277
|
+
ws.onmessage?.({
|
|
1278
|
+
data: JSON.stringify({
|
|
1279
|
+
type: "subscribe",
|
|
1280
|
+
id: "sub-1",
|
|
1281
|
+
operation: "getUser",
|
|
1282
|
+
input: { id: "user-1" },
|
|
1283
|
+
fields: ["name"],
|
|
1284
|
+
}),
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1288
|
+
|
|
1289
|
+
// Upgrade to full subscription
|
|
1290
|
+
ws.onmessage?.({
|
|
1291
|
+
data: JSON.stringify({
|
|
1292
|
+
type: "updateFields",
|
|
1293
|
+
id: "sub-1",
|
|
1294
|
+
addFields: ["*"],
|
|
1295
|
+
}),
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// Should not throw
|
|
1299
|
+
expect(true).toBe(true);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it("downgrades from wildcard to specific fields", async () => {
|
|
1303
|
+
const getUser = query()
|
|
1304
|
+
.input(z.object({ id: z.string() }))
|
|
1305
|
+
.returns(User)
|
|
1306
|
+
.resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
|
|
1307
|
+
|
|
1308
|
+
const server = createServer({
|
|
1309
|
+
entities: { User },
|
|
1310
|
+
queries: { getUser },
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
const ws = createMockWs();
|
|
1314
|
+
server.handleWebSocket(ws);
|
|
1315
|
+
|
|
1316
|
+
// Subscribe with wildcard
|
|
1317
|
+
ws.onmessage?.({
|
|
1318
|
+
data: JSON.stringify({
|
|
1319
|
+
type: "subscribe",
|
|
1320
|
+
id: "sub-1",
|
|
1321
|
+
operation: "getUser",
|
|
1322
|
+
input: { id: "user-1" },
|
|
1323
|
+
fields: "*",
|
|
1324
|
+
}),
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1328
|
+
|
|
1329
|
+
// Downgrade to specific fields
|
|
1330
|
+
ws.onmessage?.({
|
|
1331
|
+
data: JSON.stringify({
|
|
1332
|
+
type: "updateFields",
|
|
1333
|
+
id: "sub-1",
|
|
1334
|
+
setFields: ["name", "email"],
|
|
1335
|
+
}),
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
// Should not throw
|
|
1339
|
+
expect(true).toBe(true);
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
it("ignores add/remove when already subscribed to wildcard", async () => {
|
|
1343
|
+
const getUser = query()
|
|
1344
|
+
.input(z.object({ id: z.string() }))
|
|
1345
|
+
.returns(User)
|
|
1346
|
+
.resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
|
|
1347
|
+
|
|
1348
|
+
const server = createServer({
|
|
1349
|
+
entities: { User },
|
|
1350
|
+
queries: { getUser },
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
const ws = createMockWs();
|
|
1354
|
+
server.handleWebSocket(ws);
|
|
1355
|
+
|
|
1356
|
+
// Subscribe with wildcard
|
|
1357
|
+
ws.onmessage?.({
|
|
1358
|
+
data: JSON.stringify({
|
|
1359
|
+
type: "subscribe",
|
|
1360
|
+
id: "sub-1",
|
|
1361
|
+
operation: "getUser",
|
|
1362
|
+
input: { id: "user-1" },
|
|
1363
|
+
fields: "*",
|
|
1364
|
+
}),
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1368
|
+
|
|
1369
|
+
// Try to add fields (should be ignored)
|
|
1370
|
+
ws.onmessage?.({
|
|
1371
|
+
data: JSON.stringify({
|
|
1372
|
+
type: "updateFields",
|
|
1373
|
+
id: "sub-1",
|
|
1374
|
+
addFields: ["bio"],
|
|
1375
|
+
}),
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
// Should not throw
|
|
1379
|
+
expect(true).toBe(true);
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
it("handles async generator with empty stream", async () => {
|
|
1383
|
+
const emptyStream = query()
|
|
1384
|
+
.returns(User)
|
|
1385
|
+
.resolve(async function* () {
|
|
1386
|
+
// Empty generator - yields nothing
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
const server = createServer({
|
|
1390
|
+
entities: { User },
|
|
1391
|
+
queries: { emptyStream },
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
await expect(server.executeQuery("emptyStream")).rejects.toThrow("returned empty stream");
|
|
1395
|
+
});
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
// =============================================================================
|
|
1399
|
+
// Test: Query with $select
|
|
1400
|
+
// =============================================================================
|
|
1401
|
+
|
|
1402
|
+
describe("Query with $select", () => {
|
|
1403
|
+
it("handles query with $select parameter", async () => {
|
|
1404
|
+
const getUser = query()
|
|
1405
|
+
.input(z.object({ id: z.string() }))
|
|
1406
|
+
.returns(User)
|
|
1407
|
+
.resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
|
|
1408
|
+
|
|
1409
|
+
const server = createServer({
|
|
1410
|
+
entities: { User },
|
|
1411
|
+
queries: { getUser },
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
// Use $select to trigger selection processing
|
|
1415
|
+
const result = await server.executeQuery("getUser", {
|
|
1416
|
+
id: "user-1",
|
|
1417
|
+
$select: { name: true, email: true },
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
expect(result).toBeDefined();
|
|
1421
|
+
expect((result as any).id).toBe("user-1");
|
|
1422
|
+
expect((result as any).name).toBe("Alice");
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
it("processes WebSocket query message with select", async () => {
|
|
1426
|
+
const getUser = query()
|
|
1427
|
+
.input(z.object({ id: z.string() }))
|
|
1428
|
+
.returns(User)
|
|
1429
|
+
.resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
|
|
1430
|
+
|
|
1431
|
+
const server = createServer({
|
|
1432
|
+
entities: { User },
|
|
1433
|
+
queries: { getUser },
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
const ws = createMockWs();
|
|
1437
|
+
server.handleWebSocket(ws);
|
|
1438
|
+
|
|
1439
|
+
// Query with select
|
|
1440
|
+
ws.onmessage?.({
|
|
1441
|
+
data: JSON.stringify({
|
|
1442
|
+
type: "query",
|
|
1443
|
+
id: "q-1",
|
|
1444
|
+
operation: "getUser",
|
|
1445
|
+
input: { id: "user-1" },
|
|
1446
|
+
select: { name: true, email: true },
|
|
1447
|
+
}),
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1451
|
+
|
|
1452
|
+
expect(ws.messages.length).toBe(1);
|
|
1453
|
+
const response = JSON.parse(ws.messages[0]);
|
|
1454
|
+
expect(response.type).toBe("result");
|
|
1455
|
+
expect(response.data.name).toBe("Alice");
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
it("applies field selection with fields array", async () => {
|
|
1459
|
+
const getUser = query()
|
|
1460
|
+
.input(z.object({ id: z.string() }))
|
|
1461
|
+
.returns(User)
|
|
1462
|
+
.resolve(({ input }) => mockUsers.find((u) => u.id === input.id) ?? null);
|
|
1463
|
+
|
|
1464
|
+
const server = createServer({
|
|
1465
|
+
entities: { User },
|
|
1466
|
+
queries: { getUser },
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
const ws = createMockWs();
|
|
1470
|
+
server.handleWebSocket(ws);
|
|
1471
|
+
|
|
1472
|
+
// Query with fields array (backward compat)
|
|
1473
|
+
ws.onmessage?.({
|
|
1474
|
+
data: JSON.stringify({
|
|
1475
|
+
type: "query",
|
|
1476
|
+
id: "q-1",
|
|
1477
|
+
operation: "getUser",
|
|
1478
|
+
input: { id: "user-1" },
|
|
1479
|
+
fields: ["name"],
|
|
1480
|
+
}),
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1484
|
+
|
|
1485
|
+
expect(ws.messages.length).toBe(1);
|
|
1486
|
+
const response = JSON.parse(ws.messages[0]);
|
|
1487
|
+
expect(response.type).toBe("result");
|
|
1488
|
+
expect(response.data.id).toBe("user-1"); // id is always included
|
|
1489
|
+
expect(response.data.name).toBe("Alice");
|
|
1490
|
+
});
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
// =============================================================================
|
|
1494
|
+
// Test: Logger integration
|
|
1495
|
+
// =============================================================================
|
|
1496
|
+
|
|
1497
|
+
describe("Logger integration", () => {
|
|
1498
|
+
it("calls logger.error on cleanup errors", async () => {
|
|
1499
|
+
const errorLogs: string[] = [];
|
|
1500
|
+
const liveQuery = query()
|
|
1501
|
+
.returns(User)
|
|
1502
|
+
.resolve(({ onCleanup }) => {
|
|
1503
|
+
onCleanup(() => {
|
|
1504
|
+
throw new Error("Cleanup failed");
|
|
1505
|
+
});
|
|
1506
|
+
return mockUsers[0];
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
const server = createServer({
|
|
1510
|
+
entities: { User },
|
|
1511
|
+
queries: { liveQuery },
|
|
1512
|
+
logger: {
|
|
1513
|
+
error: (msg, ...args) => {
|
|
1514
|
+
errorLogs.push(`${msg} ${args.join(" ")}`);
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
const ws = createMockWs();
|
|
1520
|
+
server.handleWebSocket(ws);
|
|
1521
|
+
|
|
1522
|
+
// Subscribe
|
|
1523
|
+
ws.onmessage?.({
|
|
1524
|
+
data: JSON.stringify({
|
|
1525
|
+
type: "subscribe",
|
|
1526
|
+
id: "sub-1",
|
|
1527
|
+
operation: "liveQuery",
|
|
1528
|
+
fields: "*",
|
|
1529
|
+
}),
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1533
|
+
|
|
1534
|
+
// Unsubscribe (triggers cleanup error)
|
|
1535
|
+
ws.onmessage?.({
|
|
1536
|
+
data: JSON.stringify({ type: "unsubscribe", id: "sub-1" }),
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
expect(errorLogs.length).toBeGreaterThan(0);
|
|
1540
|
+
expect(errorLogs[0]).toContain("Cleanup error");
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
it("calls logger.error on disconnect cleanup errors", async () => {
|
|
1544
|
+
const errorLogs: string[] = [];
|
|
1545
|
+
const liveQuery = query()
|
|
1546
|
+
.returns(User)
|
|
1547
|
+
.resolve(({ onCleanup }) => {
|
|
1548
|
+
onCleanup(() => {
|
|
1549
|
+
throw new Error("Disconnect cleanup failed");
|
|
1550
|
+
});
|
|
1551
|
+
return mockUsers[0];
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
const server = createServer({
|
|
1555
|
+
entities: { User },
|
|
1556
|
+
queries: { liveQuery },
|
|
1557
|
+
logger: {
|
|
1558
|
+
error: (msg, ...args) => {
|
|
1559
|
+
errorLogs.push(`${msg} ${args.join(" ")}`);
|
|
1560
|
+
},
|
|
1561
|
+
},
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
const ws = createMockWs();
|
|
1565
|
+
server.handleWebSocket(ws);
|
|
1566
|
+
|
|
1567
|
+
// Subscribe
|
|
1568
|
+
ws.onmessage?.({
|
|
1569
|
+
data: JSON.stringify({
|
|
1570
|
+
type: "subscribe",
|
|
1571
|
+
id: "sub-1",
|
|
1572
|
+
operation: "liveQuery",
|
|
1573
|
+
fields: "*",
|
|
1574
|
+
}),
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1578
|
+
|
|
1579
|
+
// Disconnect (triggers cleanup)
|
|
1580
|
+
ws.onclose?.();
|
|
1581
|
+
|
|
1582
|
+
expect(errorLogs.length).toBeGreaterThan(0);
|
|
1583
|
+
expect(errorLogs[0]).toContain("Cleanup error");
|
|
1584
|
+
});
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
// =============================================================================
|
|
1588
|
+
// Test: Entity Resolvers
|
|
1589
|
+
// =============================================================================
|
|
1590
|
+
|
|
1591
|
+
describe("Entity Resolvers", () => {
|
|
1592
|
+
it("executes field resolvers with select", async () => {
|
|
1593
|
+
const Author = entity("Author", {
|
|
1594
|
+
id: t.id(),
|
|
1595
|
+
name: t.string(),
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
const Article = entity("Article", {
|
|
1599
|
+
id: t.id(),
|
|
1600
|
+
title: t.string(),
|
|
1601
|
+
authorId: t.string(),
|
|
1602
|
+
// author relation is resolved
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
const mockAuthors = [
|
|
1606
|
+
{ id: "author-1", name: "Alice" },
|
|
1607
|
+
{ id: "author-2", name: "Bob" },
|
|
1608
|
+
];
|
|
1609
|
+
|
|
1610
|
+
const mockArticles = [
|
|
1611
|
+
{ id: "article-1", title: "First Post", authorId: "author-1" },
|
|
1612
|
+
{ id: "article-2", title: "Second Post", authorId: "author-2" },
|
|
1613
|
+
];
|
|
1614
|
+
|
|
1615
|
+
// Create resolver for Article entity
|
|
1616
|
+
const articleResolver = resolver(Article, (f) => ({
|
|
1617
|
+
id: f.expose("id"),
|
|
1618
|
+
title: f.expose("title"),
|
|
1619
|
+
author: f.one(Author).resolve(({ parent }) => {
|
|
1620
|
+
return mockAuthors.find((a) => a.id === parent.authorId) ?? null;
|
|
1621
|
+
}),
|
|
1622
|
+
}));
|
|
1623
|
+
|
|
1624
|
+
const getArticle = query()
|
|
1625
|
+
.input(z.object({ id: z.string() }))
|
|
1626
|
+
.returns(Article)
|
|
1627
|
+
.resolve(({ input }) => {
|
|
1628
|
+
return mockArticles.find((a) => a.id === input.id) ?? null;
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
const server = createServer({
|
|
1632
|
+
entities: { Article, Author },
|
|
1633
|
+
queries: { getArticle },
|
|
1634
|
+
resolvers: [articleResolver],
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
const result = await server.executeQuery("getArticle", {
|
|
1638
|
+
id: "article-1",
|
|
1639
|
+
$select: {
|
|
1640
|
+
title: true,
|
|
1641
|
+
author: {
|
|
1642
|
+
select: {
|
|
1643
|
+
name: true,
|
|
1644
|
+
},
|
|
1645
|
+
},
|
|
1646
|
+
},
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
expect(result).toBeDefined();
|
|
1650
|
+
expect((result as any).title).toBe("First Post");
|
|
1651
|
+
expect((result as any).author).toBeDefined();
|
|
1652
|
+
expect((result as any).author.name).toBe("Alice");
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
it("handles array relations in resolvers", async () => {
|
|
1656
|
+
const Author = entity("Author", {
|
|
1657
|
+
id: t.id(),
|
|
1658
|
+
name: t.string(),
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
const Article = entity("Article", {
|
|
1662
|
+
id: t.id(),
|
|
1663
|
+
title: t.string(),
|
|
1664
|
+
authorId: t.string(),
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
const mockArticles = [
|
|
1668
|
+
{ id: "article-1", title: "First Post", authorId: "author-1" },
|
|
1669
|
+
{ id: "article-2", title: "Second Post", authorId: "author-1" },
|
|
1670
|
+
{ id: "article-3", title: "Third Post", authorId: "author-2" },
|
|
1671
|
+
];
|
|
1672
|
+
|
|
1673
|
+
// Create resolver for Author entity with articles relation
|
|
1674
|
+
const authorResolver = resolver(Author, (f) => ({
|
|
1675
|
+
id: f.expose("id"),
|
|
1676
|
+
name: f.expose("name"),
|
|
1677
|
+
articles: f.many(Article).resolve(({ parent }) => {
|
|
1678
|
+
return mockArticles.filter((a) => a.authorId === parent.id);
|
|
1679
|
+
}),
|
|
1680
|
+
}));
|
|
1681
|
+
|
|
1682
|
+
const getAuthor = query()
|
|
1683
|
+
.input(z.object({ id: z.string() }))
|
|
1684
|
+
.returns(Author)
|
|
1685
|
+
.resolve(({ input }) => {
|
|
1686
|
+
return { id: input.id, name: input.id === "author-1" ? "Alice" : "Bob" };
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
const server = createServer({
|
|
1690
|
+
entities: { Article, Author },
|
|
1691
|
+
queries: { getAuthor },
|
|
1692
|
+
resolvers: [authorResolver],
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
const result = await server.executeQuery("getAuthor", {
|
|
1696
|
+
id: "author-1",
|
|
1697
|
+
$select: {
|
|
1698
|
+
name: true,
|
|
1699
|
+
articles: {
|
|
1700
|
+
select: {
|
|
1701
|
+
title: true,
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
},
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
expect(result).toBeDefined();
|
|
1708
|
+
expect((result as any).name).toBe("Alice");
|
|
1709
|
+
expect((result as any).articles).toBeDefined();
|
|
1710
|
+
expect(Array.isArray((result as any).articles)).toBe(true);
|
|
1711
|
+
expect((result as any).articles.length).toBe(2);
|
|
1712
|
+
expect((result as any).articles[0].title).toBe("First Post");
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
it("handles field resolver with args", async () => {
|
|
1716
|
+
const User = entity("User", {
|
|
1717
|
+
id: t.id(),
|
|
1718
|
+
name: t.string(),
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
const userResolver = resolver(User, (f) => ({
|
|
1722
|
+
id: f.expose("id"),
|
|
1723
|
+
name: f.expose("name"),
|
|
1724
|
+
greeting: f
|
|
1725
|
+
.string()
|
|
1726
|
+
.args<{ formal: boolean }>()
|
|
1727
|
+
.resolve(({ parent, args }) => {
|
|
1728
|
+
return args.formal ? `Good day, ${parent.name}` : `Hey ${parent.name}!`;
|
|
1729
|
+
}),
|
|
1730
|
+
}));
|
|
1731
|
+
|
|
1732
|
+
const getUser = query()
|
|
1733
|
+
.returns(User)
|
|
1734
|
+
.resolve(() => ({ id: "1", name: "Alice" }));
|
|
1735
|
+
|
|
1736
|
+
const server = createServer({
|
|
1737
|
+
entities: { User },
|
|
1738
|
+
queries: { getUser },
|
|
1739
|
+
resolvers: [userResolver],
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
const result = await server.executeQuery("getUser", {
|
|
1743
|
+
$select: {
|
|
1744
|
+
name: true,
|
|
1745
|
+
greeting: {
|
|
1746
|
+
args: { formal: true },
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
expect(result).toBeDefined();
|
|
1752
|
+
expect((result as any).greeting).toBe("Good day, Alice");
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
it("returns data unchanged when no select provided", async () => {
|
|
1756
|
+
const User = entity("User", {
|
|
1757
|
+
id: t.id(),
|
|
1758
|
+
name: t.string(),
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
const userResolver = resolver(User, (f) => ({
|
|
1762
|
+
id: f.expose("id"),
|
|
1763
|
+
name: f.expose("name"),
|
|
1764
|
+
bio: f.string().resolve(({ parent }) => `Biography of ${parent.name}`),
|
|
1765
|
+
}));
|
|
1766
|
+
|
|
1767
|
+
const getUser = query()
|
|
1768
|
+
.returns(User)
|
|
1769
|
+
.resolve(() => ({ id: "1", name: "Alice" }));
|
|
1770
|
+
|
|
1771
|
+
const server = createServer({
|
|
1772
|
+
entities: { User },
|
|
1773
|
+
queries: { getUser },
|
|
1774
|
+
resolvers: [userResolver],
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
const result = await server.executeQuery("getUser");
|
|
1778
|
+
|
|
1779
|
+
expect(result).toBeDefined();
|
|
1780
|
+
expect((result as any).name).toBe("Alice");
|
|
1781
|
+
// bio should not be resolved without select
|
|
1782
|
+
expect((result as any).bio).toBeUndefined();
|
|
1783
|
+
});
|
|
807
1784
|
});
|