@sylphx/lens-server 3.0.1 → 4.0.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 +16 -7
- package/dist/index.js +318 -114
- package/package.json +2 -2
- package/src/e2e/server.test.ts +70 -56
- package/src/handlers/framework.ts +65 -32
- package/src/handlers/http.test.ts +8 -8
- package/src/handlers/http.ts +3 -5
- package/src/handlers/ws-types.ts +1 -0
- package/src/handlers/ws.test.ts +6 -6
- package/src/handlers/ws.ts +14 -3
- package/src/index.ts +0 -2
- package/src/plugin/optimistic.ts +6 -6
- package/src/reconnect/operation-log.ts +20 -9
- package/src/server/create.test.ts +223 -316
- package/src/server/create.ts +328 -123
- package/src/server/types.ts +23 -6
- package/src/storage/memory.ts +24 -3
|
@@ -13,17 +13,21 @@
|
|
|
13
13
|
import { describe, expect, it } from "bun:test";
|
|
14
14
|
import {
|
|
15
15
|
applyOps,
|
|
16
|
-
|
|
16
|
+
boolean,
|
|
17
17
|
firstValueFrom,
|
|
18
|
+
id,
|
|
18
19
|
isError,
|
|
19
20
|
isOps,
|
|
20
21
|
isSnapshot,
|
|
22
|
+
list,
|
|
21
23
|
type Message,
|
|
24
|
+
model,
|
|
22
25
|
mutation,
|
|
26
|
+
nullable,
|
|
23
27
|
query,
|
|
24
28
|
resolver,
|
|
25
29
|
router,
|
|
26
|
-
|
|
30
|
+
string,
|
|
27
31
|
} from "@sylphx/lens-core";
|
|
28
32
|
import { z } from "zod";
|
|
29
33
|
import { optimisticPlugin } from "../plugin/optimistic.js";
|
|
@@ -76,10 +80,10 @@ function createResultsCollector() {
|
|
|
76
80
|
// Test Entities
|
|
77
81
|
// =============================================================================
|
|
78
82
|
|
|
79
|
-
const User =
|
|
80
|
-
id:
|
|
81
|
-
name:
|
|
82
|
-
email:
|
|
83
|
+
const User = model("User", {
|
|
84
|
+
id: id(),
|
|
85
|
+
name: string(),
|
|
86
|
+
email: nullable(string()),
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
// =============================================================================
|
|
@@ -89,8 +93,8 @@ const User = entity("User", {
|
|
|
89
93
|
const getUser = query()
|
|
90
94
|
.input(z.object({ id: z.string() }))
|
|
91
95
|
.returns(User)
|
|
92
|
-
.resolve(({
|
|
93
|
-
id:
|
|
96
|
+
.resolve(({ args }) => ({
|
|
97
|
+
id: args.id,
|
|
94
98
|
name: "Test User",
|
|
95
99
|
email: "test@example.com",
|
|
96
100
|
}));
|
|
@@ -103,18 +107,18 @@ const getUsers = query().resolve(() => [
|
|
|
103
107
|
const createUser = mutation()
|
|
104
108
|
.input(z.object({ name: z.string(), email: z.string().optional() }))
|
|
105
109
|
.returns(User)
|
|
106
|
-
.resolve(({
|
|
110
|
+
.resolve(({ args }) => ({
|
|
107
111
|
id: "new-id",
|
|
108
|
-
name:
|
|
109
|
-
email:
|
|
112
|
+
name: args.name,
|
|
113
|
+
email: args.email,
|
|
110
114
|
}));
|
|
111
115
|
|
|
112
116
|
const updateUser = mutation()
|
|
113
117
|
.input(z.object({ id: z.string(), name: z.string().optional() }))
|
|
114
118
|
.returns(User)
|
|
115
|
-
.resolve(({
|
|
116
|
-
id:
|
|
117
|
-
name:
|
|
119
|
+
.resolve(({ args }) => ({
|
|
120
|
+
id: args.id,
|
|
121
|
+
name: args.name ?? "Updated",
|
|
118
122
|
}));
|
|
119
123
|
|
|
120
124
|
const deleteUser = mutation()
|
|
@@ -531,17 +535,17 @@ describe("type inference", () => {
|
|
|
531
535
|
|
|
532
536
|
describe("field resolvers", () => {
|
|
533
537
|
// Test entities for field resolver tests
|
|
534
|
-
const Author =
|
|
535
|
-
id:
|
|
536
|
-
name:
|
|
538
|
+
const Author = model("Author", {
|
|
539
|
+
id: id(),
|
|
540
|
+
name: string(),
|
|
537
541
|
});
|
|
538
542
|
|
|
539
|
-
const Post =
|
|
540
|
-
id:
|
|
541
|
-
title:
|
|
542
|
-
content:
|
|
543
|
-
published:
|
|
544
|
-
authorId:
|
|
543
|
+
const Post = model("Post", {
|
|
544
|
+
id: id(),
|
|
545
|
+
title: string(),
|
|
546
|
+
content: string(),
|
|
547
|
+
published: boolean(),
|
|
548
|
+
authorId: string(),
|
|
545
549
|
});
|
|
546
550
|
|
|
547
551
|
// Mock data
|
|
@@ -562,19 +566,19 @@ describe("field resolvers", () => {
|
|
|
562
566
|
|
|
563
567
|
it("resolves field with nested input args (like GraphQL)", async () => {
|
|
564
568
|
// Define field resolver with args (like GraphQL)
|
|
565
|
-
const authorResolver = resolver<TestContext>()(Author, (
|
|
566
|
-
id:
|
|
567
|
-
name:
|
|
568
|
-
|
|
569
|
-
|
|
569
|
+
const authorResolver = resolver<TestContext>()(Author, (t) => ({
|
|
570
|
+
id: t.expose("id"),
|
|
571
|
+
name: t.expose("name"),
|
|
572
|
+
// New API: t.args().resolve() without type method
|
|
573
|
+
posts: t
|
|
570
574
|
.args(
|
|
571
575
|
z.object({
|
|
572
576
|
limit: z.number().optional(),
|
|
573
577
|
published: z.boolean().optional(),
|
|
574
578
|
}),
|
|
575
579
|
)
|
|
576
|
-
.resolve(({
|
|
577
|
-
let posts = ctx.db.posts.filter((p) => p.authorId ===
|
|
580
|
+
.resolve(({ source, args, ctx }) => {
|
|
581
|
+
let posts = ctx.db.posts.filter((p) => p.authorId === source.id);
|
|
578
582
|
if (args.published !== undefined) {
|
|
579
583
|
posts = posts.filter((p) => p.published === args.published);
|
|
580
584
|
}
|
|
@@ -588,8 +592,8 @@ describe("field resolvers", () => {
|
|
|
588
592
|
const getAuthor = query<TestContext>()
|
|
589
593
|
.input(z.object({ id: z.string() }))
|
|
590
594
|
.returns(Author)
|
|
591
|
-
.resolve(({
|
|
592
|
-
const author = ctx.db.authors.find((a) => a.id ===
|
|
595
|
+
.resolve(({ args, ctx }) => {
|
|
596
|
+
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
593
597
|
if (!author) throw new Error("Author not found");
|
|
594
598
|
return author;
|
|
595
599
|
});
|
|
@@ -634,20 +638,21 @@ describe("field resolvers", () => {
|
|
|
634
638
|
it("passes context to field resolvers", async () => {
|
|
635
639
|
let capturedContext: TestContext | null = null;
|
|
636
640
|
|
|
637
|
-
const authorResolver = resolver<TestContext>()(Author, (
|
|
638
|
-
id:
|
|
639
|
-
name:
|
|
640
|
-
|
|
641
|
+
const authorResolver = resolver<TestContext>()(Author, (t) => ({
|
|
642
|
+
id: t.expose("id"),
|
|
643
|
+
name: t.expose("name"),
|
|
644
|
+
// Plain function for relations (new API)
|
|
645
|
+
posts: ({ source, ctx }) => {
|
|
641
646
|
capturedContext = ctx;
|
|
642
|
-
return ctx.db.posts.filter((p) => p.authorId ===
|
|
643
|
-
}
|
|
647
|
+
return ctx.db.posts.filter((p) => p.authorId === source.id);
|
|
648
|
+
},
|
|
644
649
|
}));
|
|
645
650
|
|
|
646
651
|
const getAuthor = query<TestContext>()
|
|
647
652
|
.input(z.object({ id: z.string() }))
|
|
648
653
|
.returns(Author)
|
|
649
|
-
.resolve(({
|
|
650
|
-
const author = ctx.db.authors.find((a) => a.id ===
|
|
654
|
+
.resolve(({ args, ctx }) => {
|
|
655
|
+
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
651
656
|
if (!author) throw new Error("Author not found");
|
|
652
657
|
return author;
|
|
653
658
|
});
|
|
@@ -677,15 +682,33 @@ describe("field resolvers", () => {
|
|
|
677
682
|
});
|
|
678
683
|
|
|
679
684
|
it("supports nested input at multiple levels", async () => {
|
|
680
|
-
//
|
|
681
|
-
const Comment =
|
|
682
|
-
id:
|
|
683
|
-
body:
|
|
684
|
-
postId:
|
|
685
|
+
// Local models with relation fields for this test
|
|
686
|
+
const Comment = model("Comment", {
|
|
687
|
+
id: id(),
|
|
688
|
+
body: string(),
|
|
689
|
+
postId: string(),
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Post with comments relation
|
|
693
|
+
const LocalPost = model("LocalPost", {
|
|
694
|
+
id: id(),
|
|
695
|
+
title: string(),
|
|
696
|
+
comments: list(() => Comment),
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// Author with posts relation
|
|
700
|
+
const LocalAuthor = model("LocalAuthor", {
|
|
701
|
+
id: id(),
|
|
702
|
+
name: string(),
|
|
703
|
+
posts: list(() => LocalPost),
|
|
685
704
|
});
|
|
686
705
|
|
|
687
706
|
const mockDbWithComments = {
|
|
688
|
-
|
|
707
|
+
authors: [{ id: "a1", name: "Author 1" }],
|
|
708
|
+
posts: [
|
|
709
|
+
{ id: "p1", title: "Post 1", authorId: "a1" },
|
|
710
|
+
{ id: "p2", title: "Post 2", authorId: "a1" },
|
|
711
|
+
],
|
|
689
712
|
comments: [
|
|
690
713
|
{ id: "c1", body: "Comment 1", postId: "p1" },
|
|
691
714
|
{ id: "c2", body: "Comment 2", postId: "p1" },
|
|
@@ -695,47 +718,41 @@ describe("field resolvers", () => {
|
|
|
695
718
|
|
|
696
719
|
type CtxWithComments = { db: typeof mockDbWithComments };
|
|
697
720
|
|
|
698
|
-
const postResolver = resolver<CtxWithComments>()(
|
|
699
|
-
id:
|
|
700
|
-
title:
|
|
701
|
-
comments:
|
|
702
|
-
.
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
return comments;
|
|
710
|
-
}),
|
|
721
|
+
const postResolver = resolver<CtxWithComments>()(LocalPost, (t) => ({
|
|
722
|
+
id: t.expose("id"),
|
|
723
|
+
title: t.expose("title"),
|
|
724
|
+
comments: t.args(z.object({ limit: z.number().optional() })).resolve(({ source, args, ctx }) => {
|
|
725
|
+
let comments = ctx.db.comments.filter((c) => c.postId === source.id);
|
|
726
|
+
if (args.limit !== undefined) {
|
|
727
|
+
comments = comments.slice(0, args.limit);
|
|
728
|
+
}
|
|
729
|
+
return comments;
|
|
730
|
+
}),
|
|
711
731
|
}));
|
|
712
732
|
|
|
713
|
-
const authorResolver = resolver<CtxWithComments>()(
|
|
714
|
-
id:
|
|
715
|
-
name:
|
|
716
|
-
posts:
|
|
717
|
-
.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
return posts;
|
|
725
|
-
}),
|
|
733
|
+
const authorResolver = resolver<CtxWithComments>()(LocalAuthor, (t) => ({
|
|
734
|
+
id: t.expose("id"),
|
|
735
|
+
name: t.expose("name"),
|
|
736
|
+
posts: t.args(z.object({ limit: z.number().optional() })).resolve(({ source, args, ctx }) => {
|
|
737
|
+
let posts = ctx.db.posts.filter((p) => p.authorId === source.id);
|
|
738
|
+
if (args.limit !== undefined) {
|
|
739
|
+
posts = posts.slice(0, args.limit);
|
|
740
|
+
}
|
|
741
|
+
return posts;
|
|
742
|
+
}),
|
|
726
743
|
}));
|
|
727
744
|
|
|
728
745
|
const getAuthor = query<CtxWithComments>()
|
|
729
746
|
.input(z.object({ id: z.string() }))
|
|
730
|
-
.returns(
|
|
731
|
-
.resolve(({
|
|
732
|
-
const author = ctx.db.authors.find((a) => a.id ===
|
|
747
|
+
.returns(LocalAuthor)
|
|
748
|
+
.resolve(({ args, ctx }) => {
|
|
749
|
+
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
733
750
|
if (!author) throw new Error("Author not found");
|
|
734
751
|
return author;
|
|
735
752
|
});
|
|
736
753
|
|
|
737
754
|
const server = createApp({
|
|
738
|
-
entities: {
|
|
755
|
+
entities: { LocalAuthor, LocalPost, Comment },
|
|
739
756
|
queries: { getAuthor },
|
|
740
757
|
resolvers: [authorResolver, postResolver],
|
|
741
758
|
context: () => ({ db: mockDbWithComments }),
|
|
@@ -773,22 +790,19 @@ describe("field resolvers", () => {
|
|
|
773
790
|
});
|
|
774
791
|
|
|
775
792
|
it("works without nested input (default args)", async () => {
|
|
776
|
-
const authorResolver = resolver<TestContext>()(Author, (
|
|
777
|
-
id:
|
|
778
|
-
name:
|
|
779
|
-
posts:
|
|
780
|
-
.
|
|
781
|
-
|
|
782
|
-
.resolve(({ parent, args, ctx }) => {
|
|
783
|
-
return ctx.db.posts.filter((p) => p.authorId === parent.id).slice(0, args.limit);
|
|
784
|
-
}),
|
|
793
|
+
const authorResolver = resolver<TestContext>()(Author, (t) => ({
|
|
794
|
+
id: t.expose("id"),
|
|
795
|
+
name: t.expose("name"),
|
|
796
|
+
posts: t.args(z.object({ limit: z.number().default(10) })).resolve(({ source, args, ctx }) => {
|
|
797
|
+
return ctx.db.posts.filter((p) => p.authorId === source.id).slice(0, args.limit);
|
|
798
|
+
}),
|
|
785
799
|
}));
|
|
786
800
|
|
|
787
801
|
const getAuthor = query<TestContext>()
|
|
788
802
|
.input(z.object({ id: z.string() }))
|
|
789
803
|
.returns(Author)
|
|
790
|
-
.resolve(({
|
|
791
|
-
const author = ctx.db.authors.find((a) => a.id ===
|
|
804
|
+
.resolve(({ args, ctx }) => {
|
|
805
|
+
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
792
806
|
if (!author) throw new Error("Author not found");
|
|
793
807
|
return author;
|
|
794
808
|
});
|
|
@@ -824,15 +838,15 @@ describe("field resolvers", () => {
|
|
|
824
838
|
// Emits forward commands to client - no re-resolution on server.
|
|
825
839
|
let postsResolverCallCount = 0;
|
|
826
840
|
|
|
827
|
-
const Author =
|
|
828
|
-
id:
|
|
829
|
-
name:
|
|
841
|
+
const Author = model("Author", {
|
|
842
|
+
id: id(),
|
|
843
|
+
name: string(),
|
|
830
844
|
});
|
|
831
845
|
|
|
832
|
-
const Post =
|
|
833
|
-
id:
|
|
834
|
-
title:
|
|
835
|
-
authorId:
|
|
846
|
+
const Post = model("Post", {
|
|
847
|
+
id: id(),
|
|
848
|
+
title: string(),
|
|
849
|
+
authorId: string(),
|
|
836
850
|
});
|
|
837
851
|
|
|
838
852
|
const mockDb = {
|
|
@@ -843,26 +857,30 @@ describe("field resolvers", () => {
|
|
|
843
857
|
],
|
|
844
858
|
};
|
|
845
859
|
|
|
846
|
-
const authorResolver = resolver<{ db: typeof mockDb }>()(Author, (
|
|
847
|
-
id:
|
|
848
|
-
name:
|
|
849
|
-
|
|
860
|
+
const authorResolver = resolver<{ db: typeof mockDb }>()(Author, (t) => ({
|
|
861
|
+
id: t.expose("id"),
|
|
862
|
+
name: t.expose("name"),
|
|
863
|
+
// Plain function for relations (new API)
|
|
864
|
+
posts: ({ source, ctx }) => {
|
|
850
865
|
postsResolverCallCount++;
|
|
851
|
-
return ctx.db.posts.filter((p) => p.authorId ===
|
|
852
|
-
}
|
|
866
|
+
return ctx.db.posts.filter((p) => p.authorId === source.id);
|
|
867
|
+
},
|
|
853
868
|
}));
|
|
854
869
|
|
|
855
870
|
type EmitFn = ((data: unknown) => void) & { merge: (partial: unknown) => void };
|
|
856
871
|
let capturedEmit: EmitFn | null = null;
|
|
857
872
|
|
|
858
|
-
|
|
873
|
+
// Use .resolve().subscribe() pattern - emit comes through Publisher callback, NOT ctx
|
|
874
|
+
const getAuthor = query<{ db: typeof mockDb }>()
|
|
859
875
|
.input(z.object({ id: z.string() }))
|
|
860
876
|
.returns(Author)
|
|
861
|
-
.resolve(({
|
|
862
|
-
|
|
863
|
-
const author = ctx.db.authors.find((a) => a.id === input.id);
|
|
877
|
+
.resolve(({ args, ctx }) => {
|
|
878
|
+
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
864
879
|
if (!author) throw new Error("Author not found");
|
|
865
880
|
return author;
|
|
881
|
+
})
|
|
882
|
+
.subscribe(() => ({ emit }) => {
|
|
883
|
+
capturedEmit = emit as EmitFn;
|
|
866
884
|
});
|
|
867
885
|
|
|
868
886
|
const server = createApp({
|
|
@@ -924,15 +942,15 @@ describe("field resolvers", () => {
|
|
|
924
942
|
let cleanupCalled = false;
|
|
925
943
|
let resolverReceivedOnCleanup = false;
|
|
926
944
|
|
|
927
|
-
const Author =
|
|
928
|
-
id:
|
|
929
|
-
name:
|
|
945
|
+
const Author = model("Author", {
|
|
946
|
+
id: id(),
|
|
947
|
+
name: string(),
|
|
930
948
|
});
|
|
931
949
|
|
|
932
|
-
const Post =
|
|
933
|
-
id:
|
|
934
|
-
title:
|
|
935
|
-
authorId:
|
|
950
|
+
const Post = model("Post", {
|
|
951
|
+
id: id(),
|
|
952
|
+
title: string(),
|
|
953
|
+
authorId: string(),
|
|
936
954
|
});
|
|
937
955
|
|
|
938
956
|
const mockDb = {
|
|
@@ -943,14 +961,13 @@ describe("field resolvers", () => {
|
|
|
943
961
|
// Use .resolve().subscribe() to get onCleanup access
|
|
944
962
|
// Per ADR-002: .resolve() alone is pure/batchable, no emit/onCleanup
|
|
945
963
|
// .resolve().subscribe() gives initial value + subscription capabilities
|
|
946
|
-
const authorResolver = resolver<{ db: typeof mockDb }>()(Author, (
|
|
947
|
-
id:
|
|
948
|
-
name:
|
|
949
|
-
posts:
|
|
950
|
-
.
|
|
951
|
-
.resolve(({ parent, ctx }) => {
|
|
964
|
+
const authorResolver = resolver<{ db: typeof mockDb }>()(Author, (t) => ({
|
|
965
|
+
id: t.expose("id"),
|
|
966
|
+
name: t.expose("name"),
|
|
967
|
+
posts: t
|
|
968
|
+
.resolve(({ source, ctx }) => {
|
|
952
969
|
// Initial resolution (batchable, no emit/onCleanup)
|
|
953
|
-
return ctx.db.posts.filter((p) => p.authorId ===
|
|
970
|
+
return ctx.db.posts.filter((p) => p.authorId === source.id);
|
|
954
971
|
})
|
|
955
972
|
.subscribe(() => ({ onCleanup }) => {
|
|
956
973
|
// Publisher pattern: emit/onCleanup come from callback, not ctx
|
|
@@ -966,8 +983,8 @@ describe("field resolvers", () => {
|
|
|
966
983
|
const getAuthor = query<{ db: typeof mockDb }>()
|
|
967
984
|
.input(z.object({ id: z.string() }))
|
|
968
985
|
.returns(Author)
|
|
969
|
-
.resolve(({
|
|
970
|
-
const author = ctx.db.authors.find((a) => a.id ===
|
|
986
|
+
.resolve(({ args, ctx }) => {
|
|
987
|
+
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
971
988
|
if (!author) throw new Error("Author not found");
|
|
972
989
|
return author;
|
|
973
990
|
});
|
|
@@ -1008,15 +1025,15 @@ describe("field resolvers", () => {
|
|
|
1008
1025
|
|
|
1009
1026
|
it("field-level emit sends update command (stateless)", async () => {
|
|
1010
1027
|
// STATELESS: Field emit sends update command with field path prefix
|
|
1011
|
-
const Author =
|
|
1012
|
-
id:
|
|
1013
|
-
name:
|
|
1028
|
+
const Author = model("Author", {
|
|
1029
|
+
id: id(),
|
|
1030
|
+
name: string(),
|
|
1014
1031
|
});
|
|
1015
1032
|
|
|
1016
|
-
const Post =
|
|
1017
|
-
id:
|
|
1018
|
-
title:
|
|
1019
|
-
authorId:
|
|
1033
|
+
const Post = model("Post", {
|
|
1034
|
+
id: id(),
|
|
1035
|
+
title: string(),
|
|
1036
|
+
authorId: string(),
|
|
1020
1037
|
});
|
|
1021
1038
|
|
|
1022
1039
|
const mockDb = {
|
|
@@ -1033,14 +1050,13 @@ describe("field resolvers", () => {
|
|
|
1033
1050
|
// Use .resolve().subscribe() to get emit access
|
|
1034
1051
|
// Per ADR-002: .resolve() alone is pure/batchable, no emit/onCleanup
|
|
1035
1052
|
// .resolve().subscribe() gives initial value + subscription capabilities
|
|
1036
|
-
const authorResolver = resolver<{ db: typeof mockDb }>()(Author, (
|
|
1037
|
-
id:
|
|
1038
|
-
name:
|
|
1039
|
-
posts:
|
|
1040
|
-
.
|
|
1041
|
-
.resolve(({ parent, ctx }) => {
|
|
1053
|
+
const authorResolver = resolver<{ db: typeof mockDb }>()(Author, (t) => ({
|
|
1054
|
+
id: t.expose("id"),
|
|
1055
|
+
name: t.expose("name"),
|
|
1056
|
+
posts: t
|
|
1057
|
+
.resolve(({ source, ctx }) => {
|
|
1042
1058
|
// Initial resolution (batchable, no emit)
|
|
1043
|
-
return ctx.db.posts.filter((p) => p.authorId ===
|
|
1059
|
+
return ctx.db.posts.filter((p) => p.authorId === source.id);
|
|
1044
1060
|
})
|
|
1045
1061
|
.subscribe(() => ({ emit, onCleanup }) => {
|
|
1046
1062
|
// Publisher pattern: emit/onCleanup come from callback
|
|
@@ -1056,8 +1072,8 @@ describe("field resolvers", () => {
|
|
|
1056
1072
|
const getAuthor = query<{ db: typeof mockDb }>()
|
|
1057
1073
|
.input(z.object({ id: z.string() }))
|
|
1058
1074
|
.returns(Author)
|
|
1059
|
-
.resolve(({
|
|
1060
|
-
const author = ctx.db.authors.find((a) => a.id ===
|
|
1075
|
+
.resolve(({ args, ctx }) => {
|
|
1076
|
+
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
1061
1077
|
if (!author) throw new Error("Author not found");
|
|
1062
1078
|
return author;
|
|
1063
1079
|
});
|
|
@@ -1128,13 +1144,15 @@ describe("field resolvers", () => {
|
|
|
1128
1144
|
type EmitFn = (data: { id: string; name: string }) => void;
|
|
1129
1145
|
let capturedEmit: EmitFn | undefined;
|
|
1130
1146
|
|
|
1131
|
-
//
|
|
1147
|
+
// Use .resolve().subscribe() pattern - emit comes through Publisher callback
|
|
1132
1148
|
const liveQuery = query()
|
|
1133
1149
|
.input(z.object({ id: z.string() }))
|
|
1134
1150
|
.returns(User)
|
|
1135
|
-
.resolve(({
|
|
1136
|
-
|
|
1137
|
-
|
|
1151
|
+
.resolve(({ args }) => {
|
|
1152
|
+
return { id: args.id, name: "Initial" };
|
|
1153
|
+
})
|
|
1154
|
+
.subscribe(() => ({ emit }) => {
|
|
1155
|
+
capturedEmit = emit as EmitFn;
|
|
1138
1156
|
});
|
|
1139
1157
|
|
|
1140
1158
|
const testRouter = router({ liveQuery });
|
|
@@ -1180,15 +1198,15 @@ describe("field resolvers", () => {
|
|
|
1180
1198
|
// .resolve() handles initial data (batchable)
|
|
1181
1199
|
// .subscribe() handles live updates (fire-and-forget)
|
|
1182
1200
|
|
|
1183
|
-
const Author =
|
|
1184
|
-
id:
|
|
1185
|
-
name:
|
|
1201
|
+
const Author = model("Author", {
|
|
1202
|
+
id: id(),
|
|
1203
|
+
name: string(),
|
|
1186
1204
|
});
|
|
1187
1205
|
|
|
1188
|
-
const Post =
|
|
1189
|
-
id:
|
|
1190
|
-
title:
|
|
1191
|
-
authorId:
|
|
1206
|
+
const Post = model("Post", {
|
|
1207
|
+
id: id(),
|
|
1208
|
+
title: string(),
|
|
1209
|
+
authorId: string(),
|
|
1192
1210
|
});
|
|
1193
1211
|
|
|
1194
1212
|
const mockDb = {
|
|
@@ -1204,16 +1222,15 @@ describe("field resolvers", () => {
|
|
|
1204
1222
|
let subscribeCallCount = 0;
|
|
1205
1223
|
let capturedFieldEmit: ((value: unknown) => void) | undefined;
|
|
1206
1224
|
|
|
1207
|
-
// Use .resolve().subscribe() pattern
|
|
1208
|
-
const authorResolver = resolver<{ db: typeof mockDb }>()(Author, (
|
|
1209
|
-
id:
|
|
1210
|
-
name:
|
|
1211
|
-
posts:
|
|
1212
|
-
.
|
|
1213
|
-
.resolve(({ parent, ctx }) => {
|
|
1225
|
+
// Use .resolve().subscribe() pattern (new API - no type method)
|
|
1226
|
+
const authorResolver = resolver<{ db: typeof mockDb }>()(Author, (t) => ({
|
|
1227
|
+
id: t.expose("id"),
|
|
1228
|
+
name: t.expose("name"),
|
|
1229
|
+
posts: t
|
|
1230
|
+
.resolve(({ source, ctx }) => {
|
|
1214
1231
|
// Phase 1: Initial resolution (batchable, no emit/onCleanup)
|
|
1215
1232
|
resolveCallCount++;
|
|
1216
|
-
return ctx.db.posts.filter((p) => p.authorId ===
|
|
1233
|
+
return ctx.db.posts.filter((p) => p.authorId === source.id);
|
|
1217
1234
|
})
|
|
1218
1235
|
.subscribe(() => ({ emit, onCleanup }) => {
|
|
1219
1236
|
// Phase 2: Publisher pattern - emit/onCleanup from callback
|
|
@@ -1229,8 +1246,8 @@ describe("field resolvers", () => {
|
|
|
1229
1246
|
const getAuthor = query<{ db: typeof mockDb }>()
|
|
1230
1247
|
.input(z.object({ id: z.string() }))
|
|
1231
1248
|
.returns(Author)
|
|
1232
|
-
.resolve(({
|
|
1233
|
-
const author = ctx.db.authors.find((a) => a.id ===
|
|
1249
|
+
.resolve(({ args, ctx }) => {
|
|
1250
|
+
const author = ctx.db.authors.find((a) => a.id === args.id);
|
|
1234
1251
|
if (!author) throw new Error("Author not found");
|
|
1235
1252
|
return author;
|
|
1236
1253
|
});
|
|
@@ -1308,7 +1325,7 @@ describe("observable behavior", () => {
|
|
|
1308
1325
|
it("delivers initial result immediately for queries", async () => {
|
|
1309
1326
|
const simpleQuery = query()
|
|
1310
1327
|
.input(z.object({ id: z.string() }))
|
|
1311
|
-
.resolve(({
|
|
1328
|
+
.resolve(({ args }) => ({ id: args.id, name: "Test" }));
|
|
1312
1329
|
|
|
1313
1330
|
const server = createApp({ queries: { simpleQuery } });
|
|
1314
1331
|
|
|
@@ -1329,11 +1346,14 @@ describe("observable behavior", () => {
|
|
|
1329
1346
|
type EmitFn = (data: unknown) => void;
|
|
1330
1347
|
let capturedEmit: EmitFn | undefined;
|
|
1331
1348
|
|
|
1349
|
+
// Use .resolve().subscribe() pattern - emit comes through Publisher callback
|
|
1332
1350
|
const liveQuery = query()
|
|
1333
1351
|
.input(z.object({ id: z.string() }))
|
|
1334
|
-
.resolve(({
|
|
1335
|
-
|
|
1336
|
-
|
|
1352
|
+
.resolve(({ args }) => {
|
|
1353
|
+
return { id: args.id, name: "Initial" };
|
|
1354
|
+
})
|
|
1355
|
+
.subscribe(() => ({ emit }) => {
|
|
1356
|
+
capturedEmit = emit as EmitFn;
|
|
1337
1357
|
});
|
|
1338
1358
|
|
|
1339
1359
|
const server = createApp({ queries: { liveQuery } });
|
|
@@ -1362,7 +1382,7 @@ describe("observable behavior", () => {
|
|
|
1362
1382
|
it("delivers mutation result via observable", async () => {
|
|
1363
1383
|
const testMutation = mutation()
|
|
1364
1384
|
.input(z.object({ name: z.string() }))
|
|
1365
|
-
.resolve(({
|
|
1385
|
+
.resolve(({ args }) => ({ id: "new", name: args.name }));
|
|
1366
1386
|
|
|
1367
1387
|
const server = createApp({ mutations: { testMutation } });
|
|
1368
1388
|
|
|
@@ -1384,9 +1404,9 @@ describe("observable behavior", () => {
|
|
|
1384
1404
|
|
|
1385
1405
|
const simpleQuery = query()
|
|
1386
1406
|
.input(z.object({ id: z.string() }))
|
|
1387
|
-
.resolve(({
|
|
1407
|
+
.resolve(({ args }) => {
|
|
1388
1408
|
resolverCalls++;
|
|
1389
|
-
return { id:
|
|
1409
|
+
return { id: args.id };
|
|
1390
1410
|
});
|
|
1391
1411
|
|
|
1392
1412
|
const server = createApp({ queries: { simpleQuery } });
|
|
@@ -1416,11 +1436,14 @@ describe("emit backpressure", () => {
|
|
|
1416
1436
|
type EmitFn = (data: unknown) => void;
|
|
1417
1437
|
let capturedEmit: EmitFn | undefined;
|
|
1418
1438
|
|
|
1439
|
+
// Use .resolve().subscribe() pattern - emit comes through Publisher callback
|
|
1419
1440
|
const liveQuery = query()
|
|
1420
1441
|
.input(z.object({ id: z.string() }))
|
|
1421
|
-
.resolve(({
|
|
1422
|
-
|
|
1423
|
-
|
|
1442
|
+
.resolve(({ args }) => {
|
|
1443
|
+
return { id: args.id, count: 0 };
|
|
1444
|
+
})
|
|
1445
|
+
.subscribe(() => ({ emit }) => {
|
|
1446
|
+
capturedEmit = emit as EmitFn;
|
|
1424
1447
|
});
|
|
1425
1448
|
|
|
1426
1449
|
const server = createApp({ queries: { liveQuery } });
|
|
@@ -1507,125 +1530,11 @@ describe("observable error handling", () => {
|
|
|
1507
1530
|
});
|
|
1508
1531
|
|
|
1509
1532
|
// =============================================================================
|
|
1510
|
-
// Unified Entity Definition (
|
|
1533
|
+
// NOTE: "Unified Entity Definition" tests for model().resolve() chain removed
|
|
1534
|
+
// as the chain API was deprecated in favor of standalone resolver() function.
|
|
1535
|
+
// See ADR-003 for the new design.
|
|
1511
1536
|
// =============================================================================
|
|
1512
1537
|
|
|
1513
|
-
describe("Unified Entity Definition", () => {
|
|
1514
|
-
describe("auto-converts entities with inline resolvers", () => {
|
|
1515
|
-
it("creates resolver from entity with inline .resolve()", async () => {
|
|
1516
|
-
// Entity with inline resolver - no separate resolver() needed
|
|
1517
|
-
const Product = entity("Product", (t) => ({
|
|
1518
|
-
id: t.id(),
|
|
1519
|
-
name: t.string(),
|
|
1520
|
-
price: t.float(),
|
|
1521
|
-
// Computed field with inline resolver
|
|
1522
|
-
displayPrice: t.string().resolve(({ parent }) => `$${(parent as { price: number }).price.toFixed(2)}`),
|
|
1523
|
-
}));
|
|
1524
|
-
|
|
1525
|
-
const getProduct = query()
|
|
1526
|
-
.input(z.object({ id: z.string() }))
|
|
1527
|
-
.returns(Product)
|
|
1528
|
-
.resolve(({ input }) => ({
|
|
1529
|
-
id: input.id,
|
|
1530
|
-
name: "Test Product",
|
|
1531
|
-
price: 19.99,
|
|
1532
|
-
}));
|
|
1533
|
-
|
|
1534
|
-
// No resolvers array needed! Server auto-detects inline resolvers
|
|
1535
|
-
const server = createApp({
|
|
1536
|
-
entities: { Product },
|
|
1537
|
-
queries: { getProduct },
|
|
1538
|
-
});
|
|
1539
|
-
|
|
1540
|
-
const result = await firstValueFrom(
|
|
1541
|
-
server.execute({
|
|
1542
|
-
path: "getProduct",
|
|
1543
|
-
input: { id: "prod-1", $select: { id: true, name: true, displayPrice: true } },
|
|
1544
|
-
}),
|
|
1545
|
-
);
|
|
1546
|
-
|
|
1547
|
-
expect(result.data).toEqual({
|
|
1548
|
-
id: "prod-1",
|
|
1549
|
-
name: "Test Product",
|
|
1550
|
-
displayPrice: "$19.99",
|
|
1551
|
-
});
|
|
1552
|
-
});
|
|
1553
|
-
|
|
1554
|
-
it("explicit resolver takes priority over inline resolver", async () => {
|
|
1555
|
-
// Entity with inline resolver
|
|
1556
|
-
const Item = entity("Item", (t) => ({
|
|
1557
|
-
id: t.id(),
|
|
1558
|
-
label: t.string().resolve(() => "inline-label"),
|
|
1559
|
-
}));
|
|
1560
|
-
|
|
1561
|
-
// Explicit resolver overrides inline
|
|
1562
|
-
const itemResolver = resolver(Item, (f) => ({
|
|
1563
|
-
id: f.expose("id"),
|
|
1564
|
-
label: f.string().resolve(() => "explicit-label"),
|
|
1565
|
-
}));
|
|
1566
|
-
|
|
1567
|
-
const getItem = query()
|
|
1568
|
-
.input(z.object({ id: z.string() }))
|
|
1569
|
-
.returns(Item)
|
|
1570
|
-
.resolve(({ input }) => ({ id: input.id }));
|
|
1571
|
-
|
|
1572
|
-
const server = createApp({
|
|
1573
|
-
entities: { Item },
|
|
1574
|
-
queries: { getItem },
|
|
1575
|
-
resolvers: [itemResolver], // Explicit resolver takes priority
|
|
1576
|
-
});
|
|
1577
|
-
|
|
1578
|
-
const result = await firstValueFrom(
|
|
1579
|
-
server.execute({
|
|
1580
|
-
path: "getItem",
|
|
1581
|
-
input: { id: "item-1", $select: { id: true, label: true } },
|
|
1582
|
-
}),
|
|
1583
|
-
);
|
|
1584
|
-
|
|
1585
|
-
expect(result.data).toEqual({
|
|
1586
|
-
id: "item-1",
|
|
1587
|
-
label: "explicit-label", // From explicit resolver, not inline
|
|
1588
|
-
});
|
|
1589
|
-
});
|
|
1590
|
-
|
|
1591
|
-
it("includes inline resolver fields in metadata", () => {
|
|
1592
|
-
const Task = entity("Task", (t) => ({
|
|
1593
|
-
id: t.id(),
|
|
1594
|
-
title: t.string(),
|
|
1595
|
-
status: t.string().resolve(({ parent }) => parent.status),
|
|
1596
|
-
}));
|
|
1597
|
-
|
|
1598
|
-
const server = createApp({
|
|
1599
|
-
entities: { Task },
|
|
1600
|
-
queries: {},
|
|
1601
|
-
});
|
|
1602
|
-
|
|
1603
|
-
const metadata = server.getMetadata();
|
|
1604
|
-
expect(metadata.entities.Task).toBeDefined();
|
|
1605
|
-
expect(metadata.entities.Task.id).toBe("exposed");
|
|
1606
|
-
expect(metadata.entities.Task.title).toBe("exposed");
|
|
1607
|
-
expect(metadata.entities.Task.status).toBe("resolve");
|
|
1608
|
-
});
|
|
1609
|
-
|
|
1610
|
-
it("skips entities without inline resolvers", () => {
|
|
1611
|
-
// Plain entity without inline resolvers
|
|
1612
|
-
const SimpleUser = entity("SimpleUser", {
|
|
1613
|
-
id: t.id(),
|
|
1614
|
-
name: t.string(),
|
|
1615
|
-
});
|
|
1616
|
-
|
|
1617
|
-
const server = createApp({
|
|
1618
|
-
entities: { SimpleUser },
|
|
1619
|
-
queries: {},
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
const metadata = server.getMetadata();
|
|
1623
|
-
// No resolver created for entities without inline resolvers
|
|
1624
|
-
expect(metadata.entities.SimpleUser).toBeUndefined();
|
|
1625
|
-
});
|
|
1626
|
-
});
|
|
1627
|
-
});
|
|
1628
|
-
|
|
1629
1538
|
// =============================================================================
|
|
1630
1539
|
// Operation-Level .resolve().subscribe() Tests (LiveQueryDef)
|
|
1631
1540
|
// =============================================================================
|
|
@@ -1638,8 +1547,8 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1638
1547
|
|
|
1639
1548
|
const liveUser = query()
|
|
1640
1549
|
.input(z.object({ id: z.string() }))
|
|
1641
|
-
.resolve(({
|
|
1642
|
-
.subscribe(({
|
|
1550
|
+
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1551
|
+
.subscribe(({ args: _args }) => ({ emit, onCleanup }) => {
|
|
1643
1552
|
subscriberCalled = true;
|
|
1644
1553
|
capturedEmit = emit;
|
|
1645
1554
|
capturedOnCleanup = onCleanup;
|
|
@@ -1678,7 +1587,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1678
1587
|
|
|
1679
1588
|
const liveUser = query()
|
|
1680
1589
|
.input(z.object({ id: z.string() }))
|
|
1681
|
-
.resolve(({
|
|
1590
|
+
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1682
1591
|
.subscribe(() => ({ emit }) => {
|
|
1683
1592
|
capturedEmit = emit;
|
|
1684
1593
|
});
|
|
@@ -1727,7 +1636,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1727
1636
|
|
|
1728
1637
|
const liveUser = query()
|
|
1729
1638
|
.input(z.object({ id: z.string() }))
|
|
1730
|
-
.resolve(({
|
|
1639
|
+
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1731
1640
|
.subscribe(() => ({ onCleanup }) => {
|
|
1732
1641
|
onCleanup(() => {
|
|
1733
1642
|
cleanupCalled = true;
|
|
@@ -1764,9 +1673,9 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1764
1673
|
|
|
1765
1674
|
const liveUser = query<TestContext>()
|
|
1766
1675
|
.input(z.object({ id: z.string() }))
|
|
1767
|
-
.resolve(({
|
|
1768
|
-
.subscribe(({
|
|
1769
|
-
receivedInput =
|
|
1676
|
+
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1677
|
+
.subscribe(({ args, ctx }) => ({ emit: _emit }) => {
|
|
1678
|
+
receivedInput = args;
|
|
1770
1679
|
receivedCtx = ctx;
|
|
1771
1680
|
});
|
|
1772
1681
|
|
|
@@ -1794,7 +1703,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1794
1703
|
it("handles subscriber errors gracefully", async () => {
|
|
1795
1704
|
const liveUser = query()
|
|
1796
1705
|
.input(z.object({ id: z.string() }))
|
|
1797
|
-
.resolve(({
|
|
1706
|
+
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1798
1707
|
.subscribe(() => () => {
|
|
1799
1708
|
throw new Error("Subscriber error");
|
|
1800
1709
|
});
|
|
@@ -1837,7 +1746,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1837
1746
|
|
|
1838
1747
|
const liveCounter = query()
|
|
1839
1748
|
.input(z.object({ id: z.string() }))
|
|
1840
|
-
.resolve(({
|
|
1749
|
+
.resolve(({ args }) => ({ id: args.id, count: 0 }))
|
|
1841
1750
|
.subscribe(() => ({ emit }) => {
|
|
1842
1751
|
capturedEmit = emit;
|
|
1843
1752
|
});
|
|
@@ -1889,7 +1798,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1889
1798
|
|
|
1890
1799
|
const liveUser = query()
|
|
1891
1800
|
.input(z.object({ id: z.string() }))
|
|
1892
|
-
.resolve(({
|
|
1801
|
+
.resolve(({ args }) => ({ id: args.id, name: "Initial", status: "offline" }))
|
|
1893
1802
|
.subscribe(() => ({ emit }) => {
|
|
1894
1803
|
capturedEmit = emit as EmitFn;
|
|
1895
1804
|
});
|
|
@@ -1936,7 +1845,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1936
1845
|
|
|
1937
1846
|
const liveUser = query()
|
|
1938
1847
|
.input(z.object({ id: z.string() }))
|
|
1939
|
-
.resolve(({
|
|
1848
|
+
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1940
1849
|
.subscribe(() => ({ emit }) => {
|
|
1941
1850
|
capturedEmit = emit;
|
|
1942
1851
|
});
|
|
@@ -1984,7 +1893,7 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
1984
1893
|
|
|
1985
1894
|
const liveUser = query()
|
|
1986
1895
|
.input(z.object({ id: z.string() }))
|
|
1987
|
-
.resolve(({
|
|
1896
|
+
.resolve(({ args }) => ({ id: args.id, name: "Initial" }))
|
|
1988
1897
|
.subscribe(() => ({ emit }) => {
|
|
1989
1898
|
subscriberCallCount++;
|
|
1990
1899
|
emits.push(emit);
|
|
@@ -2032,9 +1941,9 @@ describe("operation-level .resolve().subscribe() (LiveQueryDef)", () => {
|
|
|
2032
1941
|
describe("scalar field subscription with emit.delta()", () => {
|
|
2033
1942
|
it("provides emit.delta() for string field subscriptions", async () => {
|
|
2034
1943
|
// UserWithBio - bio field is not provided by query, resolved by field resolver
|
|
2035
|
-
const UserWithBio =
|
|
2036
|
-
id:
|
|
2037
|
-
name:
|
|
1944
|
+
const UserWithBio = model("UserWithBio", {
|
|
1945
|
+
id: id(),
|
|
1946
|
+
name: string(),
|
|
2038
1947
|
});
|
|
2039
1948
|
|
|
2040
1949
|
let capturedEmit: {
|
|
@@ -2042,14 +1951,13 @@ describe("scalar field subscription with emit.delta()", () => {
|
|
|
2042
1951
|
delta?: (operations: { position: number; insert: string }[]) => void;
|
|
2043
1952
|
} | null = null;
|
|
2044
1953
|
|
|
2045
|
-
const userResolver = resolver(UserWithBio, (
|
|
2046
|
-
id:
|
|
2047
|
-
name:
|
|
1954
|
+
const userResolver = resolver(UserWithBio, (t) => ({
|
|
1955
|
+
id: t.expose("id"),
|
|
1956
|
+
name: t.expose("name"),
|
|
2048
1957
|
// bio is a computed field with resolve + subscribe
|
|
2049
|
-
bio:
|
|
2050
|
-
.string()
|
|
1958
|
+
bio: t
|
|
2051
1959
|
.resolve(() => "Initial bio")
|
|
2052
|
-
.subscribe((
|
|
1960
|
+
.subscribe(() => ({ emit, onCleanup }) => {
|
|
2053
1961
|
capturedEmit = emit as typeof capturedEmit;
|
|
2054
1962
|
onCleanup(() => {});
|
|
2055
1963
|
}),
|
|
@@ -2061,7 +1969,7 @@ describe("scalar field subscription with emit.delta()", () => {
|
|
|
2061
1969
|
getUserWithBio: query()
|
|
2062
1970
|
.input(z.object({ id: z.string() }))
|
|
2063
1971
|
.returns(UserWithBio)
|
|
2064
|
-
.resolve(({
|
|
1972
|
+
.resolve(({ args }) => ({ id: args.id, name: "Alice" })),
|
|
2065
1973
|
},
|
|
2066
1974
|
resolvers: [userResolver],
|
|
2067
1975
|
});
|
|
@@ -2096,9 +2004,9 @@ describe("scalar field subscription with emit.delta()", () => {
|
|
|
2096
2004
|
|
|
2097
2005
|
it("emit.delta() sends delta command (stateless)", async () => {
|
|
2098
2006
|
// UserWithContent - content field resolved by field resolver
|
|
2099
|
-
const UserWithContent =
|
|
2100
|
-
id:
|
|
2101
|
-
name:
|
|
2007
|
+
const UserWithContent = model("UserWithContent", {
|
|
2008
|
+
id: id(),
|
|
2009
|
+
name: string(),
|
|
2102
2010
|
});
|
|
2103
2011
|
|
|
2104
2012
|
let capturedEmit: {
|
|
@@ -2106,14 +2014,13 @@ describe("scalar field subscription with emit.delta()", () => {
|
|
|
2106
2014
|
delta?: (operations: { position: number; insert: string }[]) => void;
|
|
2107
2015
|
} | null = null;
|
|
2108
2016
|
|
|
2109
|
-
const userResolver = resolver(UserWithContent, (
|
|
2110
|
-
id:
|
|
2111
|
-
name:
|
|
2017
|
+
const userResolver = resolver(UserWithContent, (t) => ({
|
|
2018
|
+
id: t.expose("id"),
|
|
2019
|
+
name: t.expose("name"),
|
|
2112
2020
|
// content is a computed field with resolve + subscribe
|
|
2113
|
-
content:
|
|
2114
|
-
.string()
|
|
2021
|
+
content: t
|
|
2115
2022
|
.resolve(() => "Hello")
|
|
2116
|
-
.subscribe((
|
|
2023
|
+
.subscribe(() => ({ emit, onCleanup }) => {
|
|
2117
2024
|
capturedEmit = emit as typeof capturedEmit;
|
|
2118
2025
|
onCleanup(() => {});
|
|
2119
2026
|
}),
|
|
@@ -2125,7 +2032,7 @@ describe("scalar field subscription with emit.delta()", () => {
|
|
|
2125
2032
|
getUserWithContent: query()
|
|
2126
2033
|
.input(z.object({ id: z.string() }))
|
|
2127
2034
|
.returns(UserWithContent)
|
|
2128
|
-
.resolve(({
|
|
2035
|
+
.resolve(({ args }) => ({ id: args.id, name: "Alice" })),
|
|
2129
2036
|
},
|
|
2130
2037
|
resolvers: [userResolver],
|
|
2131
2038
|
});
|