@covenant-rpc/server 0.1.3

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.
@@ -0,0 +1,238 @@
1
+ import { z } from "zod";
2
+ import { test, expect } from "bun:test";
3
+ import { declareCovenant, query, mutation } from "@covenant-rpc/core";
4
+ import { CovenantServer } from "../lib/server";
5
+ import { emptyServerToSidekick } from "../lib/interfaces/empty";
6
+ import { emptyClientToSidekick } from "@covenant-rpc/client";
7
+ import { CovenantClient } from "@covenant-rpc/client";
8
+ import { directClientToServer } from "../lib/interfaces/direct";
9
+
10
+
11
+ test("simple procedure", async () => {
12
+ const covenant = declareCovenant({
13
+ procedures: {
14
+ helloWorld: query({
15
+ input: z.object({
16
+ name: z.string(),
17
+ }),
18
+ output: z.object({
19
+ message: z.string(),
20
+ })
21
+ })
22
+ },
23
+ channels: {},
24
+ });
25
+
26
+ const server = new CovenantServer(covenant, {
27
+ contextGenerator: () => undefined,
28
+ derivation: () => { },
29
+ sidekickConnection: emptyServerToSidekick(),
30
+ });
31
+
32
+ server.defineProcedure("helloWorld", {
33
+ resources: (i) => {
34
+ return [`greeting/${i.inputs.name}`];
35
+ },
36
+ procedure: ({ inputs }) => {
37
+ return {
38
+ message: `Hello, ${inputs.name}`,
39
+ }
40
+ },
41
+ })
42
+
43
+ const client = new CovenantClient(covenant, {
44
+ sidekickConnection: emptyClientToSidekick(),
45
+ serverConnection: directClientToServer(server, {}),
46
+ });
47
+
48
+ const result = await client.query("helloWorld", {
49
+ name: "Someone",
50
+ });
51
+
52
+ expect(result.success).toBe(true);
53
+ expect(result.error).toBe(null);
54
+ expect(result.data).toEqual({
55
+ message: "Hello, Someone",
56
+ });
57
+ expect(result.resources).toEqual(["greeting/Someone"]);
58
+ });
59
+
60
+ const widgetSchema = z.object({
61
+ id: z.number(),
62
+ name: z.string(),
63
+ price: z.number(),
64
+ });
65
+ type Widget = z.infer<typeof widgetSchema>;
66
+
67
+ test("procedure with local listen", async () => {
68
+
69
+ const currentWidgets: Widget[] = [
70
+ {
71
+ id: 1,
72
+ name: "Widget one",
73
+ price: 1000000,
74
+ },
75
+ ]
76
+
77
+ const covenant = declareCovenant({
78
+ procedures: {
79
+ getWidgets: query({
80
+ input: z.null(),
81
+ output: z.array(widgetSchema),
82
+ }),
83
+ addWidget: mutation({
84
+ input: widgetSchema,
85
+ output: z.null(),
86
+ })
87
+ },
88
+ channels: {},
89
+ });
90
+
91
+ const server = new CovenantServer(covenant, {
92
+ contextGenerator: () => null,
93
+ derivation: () => { },
94
+ sidekickConnection: emptyServerToSidekick(),
95
+ });
96
+
97
+ server.defineProcedure("addWidget", {
98
+ procedure: ({ inputs }) => {
99
+ currentWidgets.push(inputs);
100
+ return null;
101
+ },
102
+ resources: () => {
103
+ return ["widgets"];
104
+ },
105
+ });
106
+
107
+ server.defineProcedure("getWidgets", {
108
+ procedure: () => {
109
+ return currentWidgets;
110
+ },
111
+ resources: () => {
112
+ return ["widgets"];
113
+ },
114
+ });
115
+
116
+ const client = new CovenantClient(covenant, {
117
+ sidekickConnection: emptyClientToSidekick(),
118
+ serverConnection: directClientToServer(server, {}),
119
+ });
120
+
121
+ // this is really ugly
122
+ const p = new Promise<void>(async (resolveOuter) => {
123
+
124
+ await new Promise<void>(async (resolve) => {
125
+ let times = 0;
126
+ const unsubscribe = client.listen("getWidgets", null, (result) => {
127
+ times += 1;
128
+ expect(result.resources).toEqual(["widgets"]);
129
+ expect(result.error).toBe(null);
130
+ expect(result.success).toBe(true);
131
+
132
+ switch (times) {
133
+ case 1:
134
+ expect(result.data).toEqual([
135
+ {
136
+ id: 1,
137
+ name: "Widget one",
138
+ price: 1000000,
139
+ },
140
+ ]);
141
+ resolveOuter();
142
+ break;
143
+ case 2:
144
+ expect(result.data).toEqual([
145
+ {
146
+ id: 1,
147
+ name: "Widget one",
148
+ price: 1000000,
149
+ },
150
+ {
151
+ id: 2,
152
+ name: "Widget two",
153
+ price: 1000000,
154
+ },
155
+ ])
156
+
157
+ resolve();
158
+ unsubscribe();
159
+ break;
160
+ }
161
+ });
162
+ });
163
+ await p;
164
+
165
+ client.mutate("addWidget", {
166
+ id: 2,
167
+ name: "Widget two",
168
+ price: 1000000,
169
+ });
170
+ });
171
+ });
172
+
173
+ test("procedure error handling", async () => {
174
+ const covenant = declareCovenant({
175
+ procedures: {
176
+ failingProcedure: query({
177
+ input: z.object({
178
+ shouldFail: z.boolean(),
179
+ }),
180
+ output: z.object({
181
+ message: z.string(),
182
+ })
183
+ })
184
+ },
185
+ channels: {},
186
+ });
187
+
188
+ const server = new CovenantServer(covenant, {
189
+ contextGenerator: () => undefined,
190
+ derivation: () => { },
191
+ sidekickConnection: emptyServerToSidekick(),
192
+ });
193
+
194
+ server.defineProcedure("failingProcedure", {
195
+ resources: () => ["test"],
196
+ procedure: ({ inputs, error }) => {
197
+ if (inputs.shouldFail) {
198
+ error("This procedure failed intentionally", 400);
199
+ }
200
+ return {
201
+ message: "Success!",
202
+ }
203
+ },
204
+ })
205
+
206
+ const client = new CovenantClient(covenant, {
207
+ sidekickConnection: emptyClientToSidekick(),
208
+ serverConnection: directClientToServer(server, {}),
209
+ });
210
+
211
+ // Test successful case
212
+ const successResult = await client.query("failingProcedure", {
213
+ shouldFail: false,
214
+ });
215
+
216
+ expect(successResult.success).toBe(true);
217
+ expect(successResult.error).toBe(null);
218
+ expect(successResult.data).toEqual({
219
+ message: "Success!",
220
+ });
221
+ expect(successResult.resources).toEqual(["test"]);
222
+
223
+ // Test error case
224
+ const errorResult = await client.query("failingProcedure", {
225
+ shouldFail: true,
226
+ });
227
+
228
+ expect(errorResult.success).toBe(false);
229
+ expect(errorResult.data).toBe(null);
230
+ expect(errorResult.resources).toBe(null);
231
+ expect(errorResult.error).toEqual({
232
+ message: "This procedure failed intentionally",
233
+ code: 400,
234
+ });
235
+ });
236
+
237
+ // TODO: test procedure with context
238
+
@@ -0,0 +1,23 @@
1
+ import { Sidekick } from "../lib/sidekick";
2
+ import { test, expect, mock } from "bun:test";
3
+ import type { SidekickOutgoingMessage } from "../lib/sidekick/protocol";
4
+
5
+
6
+ test("resource updating", () => {
7
+ // could be anything we just need to assert the calls
8
+ const publishFunction = mock(async (topic: string, message: SidekickOutgoingMessage) => {});
9
+
10
+ const sidekick = new Sidekick(publishFunction);
11
+
12
+ sidekick.updateResources(["hello", "world"]);
13
+ expect(publishFunction.mock.calls).toEqual([
14
+ ["resource:hello", {
15
+ type: "updated",
16
+ resource: "hello",
17
+ }],
18
+ ["resource:world", {
19
+ type: "updated",
20
+ resource: "world",
21
+ }],
22
+ ]);
23
+ });
@@ -0,0 +1,122 @@
1
+ import { test } from "bun:test";
2
+ import { v } from "@covenant-rpc/core/validation";
3
+
4
+ // Type-level testing helper
5
+ type Expect<T extends true> = T;
6
+ type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
7
+
8
+ test("type inference works correctly", () => {
9
+ // Basic type inference
10
+ type BoolInfer = v.Infer<ReturnType<typeof v.bool>>;
11
+ type NumberInfer = v.Infer<ReturnType<typeof v.number>>;
12
+ type StringInfer = v.Infer<ReturnType<typeof v.string>>;
13
+
14
+ type _BoolTest = Expect<Equal<BoolInfer, boolean>>;
15
+ type _NumberTest = Expect<Equal<NumberInfer, number>>;
16
+ type _StringTest = Expect<Equal<StringInfer, string>>;
17
+
18
+ // Optional type inference
19
+ type OptionalStringInfer = v.Infer<ReturnType<typeof v.optional<string>>>;
20
+ type _OptionalTest = Expect<Equal<OptionalStringInfer, string | undefined>>;
21
+
22
+ // Nullable type inference
23
+ type NullableStringInfer = v.Infer<ReturnType<typeof v.nullable<string>>>;
24
+ type _NullableTest = Expect<Equal<NullableStringInfer, string | null>>;
25
+
26
+ // Union type inference
27
+ const stringOrNumber = v.union(v.string(), v.number());
28
+ type UnionInfer = v.Infer<typeof stringOrNumber>;
29
+ type _UnionTest = Expect<Equal<UnionInfer, string | number>>;
30
+
31
+ // Literal type inference
32
+ const successLiteral = v.literal("success");
33
+ const numberLiteral = v.literal(42);
34
+ const boolLiteral = v.literal(true);
35
+ const nullLiteral = v.literal(null);
36
+
37
+ type SuccessLiteralInfer = v.Infer<typeof successLiteral>;
38
+ type NumberLiteralInfer = v.Infer<typeof numberLiteral>;
39
+ type BoolLiteralInfer = v.Infer<typeof boolLiteral>;
40
+ type NullLiteralInfer = v.Infer<typeof nullLiteral>;
41
+
42
+ type _SuccessLiteralTest = Expect<Equal<SuccessLiteralInfer, "success">>;
43
+ type _NumberLiteralTest = Expect<Equal<NumberLiteralInfer, 42>>;
44
+ type _BoolLiteralTest = Expect<Equal<BoolLiteralInfer, true>>;
45
+ type _NullLiteralTest = Expect<Equal<NullLiteralInfer, null>>;
46
+
47
+ // Object type inference
48
+ const userValidator = v.obj({
49
+ id: v.number(),
50
+ name: v.string(),
51
+ active: v.bool()
52
+ });
53
+
54
+ type UserInfer = v.Infer<typeof userValidator>;
55
+ type ExpectedUser = {
56
+ id: number;
57
+ name: string;
58
+ active: boolean;
59
+ };
60
+ type _UserTest = Expect<Equal<UserInfer, ExpectedUser>>;
61
+
62
+ // Complex nested object with optional and nullable fields
63
+ const complexValidator = v.obj({
64
+ status: v.union(v.literal("success"), v.literal("error"), v.literal("pending")),
65
+ data: v.optional(v.obj({
66
+ id: v.number(),
67
+ name: v.string(),
68
+ tags: v.nullable(v.union(v.string(), v.number()))
69
+ })),
70
+ metadata: v.nullable(v.obj({
71
+ timestamp: v.number(),
72
+ source: v.literal("api")
73
+ }))
74
+ });
75
+
76
+ type ComplexInfer = v.Infer<typeof complexValidator>;
77
+ type ExpectedComplex = {
78
+ status: "success" | "error" | "pending";
79
+ data: {
80
+ id: number;
81
+ name: string;
82
+ tags: string | number | null;
83
+ } | undefined;
84
+ metadata: {
85
+ timestamp: number;
86
+ source: "api";
87
+ } | null;
88
+ };
89
+ type _ComplexTest = Expect<Equal<ComplexInfer, ExpectedComplex>>;
90
+
91
+ // Multiple union literals
92
+ const statusValidator = v.union(
93
+ v.literal("loading"),
94
+ v.literal("success"),
95
+ v.literal("error")
96
+ );
97
+ type StatusInfer = v.Infer<typeof statusValidator>;
98
+ type _StatusTest = Expect<Equal<StatusInfer, "loading" | "success" | "error">>;
99
+
100
+ // Mixed union types
101
+ const mixedValidator = v.union(
102
+ v.literal("none"),
103
+ v.number(),
104
+ v.obj({ type: v.literal("object") })
105
+ );
106
+ type MixedInfer = v.Infer<typeof mixedValidator>;
107
+ type ExpectedMixed = "none" | number | { type: "object" };
108
+ type _MixedTest = Expect<Equal<MixedInfer, ExpectedMixed>>;
109
+
110
+ // Array type inference
111
+ const stringArrayValidator = v.array(v.string());
112
+ const numberArrayValidator = v.array(v.number());
113
+ const objArrayValidator = v.array(v.obj({ id: v.number() }));
114
+
115
+ type StringArrayInfer = v.Infer<typeof stringArrayValidator>;
116
+ type NumberArrayInfer = v.Infer<typeof numberArrayValidator>;
117
+ type ObjArrayInfer = v.Infer<typeof objArrayValidator>;
118
+
119
+ type _StringArrayTest = Expect<Equal<StringArrayInfer, string[]>>;
120
+ type _NumberArrayTest = Expect<Equal<NumberArrayInfer, number[]>>;
121
+ type _ObjArrayTest = Expect<Equal<ObjArrayInfer, { id: number }[]>>;
122
+ });
@@ -0,0 +1,144 @@
1
+ import { test, expect } from "bun:test";
2
+ import { v } from "@covenant-rpc/core/validation";
3
+
4
+ test("bool validator", () => {
5
+ const validator = v.bool();
6
+ expect(validator(true)).toBe(true);
7
+ expect(validator(false)).toBe(true);
8
+ expect(validator("true")).toBe(false);
9
+ expect(validator(1)).toBe(false);
10
+ expect(validator(null)).toBe(false);
11
+ });
12
+
13
+ test("number validator", () => {
14
+ const validator = v.number();
15
+ expect(validator(42)).toBe(true);
16
+ expect(validator(3.14)).toBe(true);
17
+ expect(validator("42")).toBe(false);
18
+ expect(validator(true)).toBe(false);
19
+ expect(validator(null)).toBe(false);
20
+ });
21
+
22
+ test("string validator", () => {
23
+ const validator = v.string();
24
+ expect(validator("hello")).toBe(true);
25
+ expect(validator("")).toBe(true);
26
+ expect(validator(42)).toBe(false);
27
+ expect(validator(null)).toBe(false);
28
+ });
29
+
30
+ test("optional validator", () => {
31
+ const validator = v.optional(v.string());
32
+ expect(validator("hello")).toBe(true);
33
+ expect(validator(undefined)).toBe(true);
34
+ expect(validator(null)).toBe(false);
35
+ expect(validator(42)).toBe(false);
36
+ });
37
+
38
+ test("nullable validator", () => {
39
+ const validator = v.nullable(v.string());
40
+ expect(validator("hello")).toBe(true);
41
+ expect(validator(null)).toBe(true);
42
+ expect(validator(undefined)).toBe(false);
43
+ expect(validator(42)).toBe(false);
44
+ });
45
+
46
+ test("union validator", () => {
47
+ const validator = v.union(v.string(), v.number());
48
+ expect(validator("hello")).toBe(true);
49
+ expect(validator(42)).toBe(true);
50
+ expect(validator(true)).toBe(false);
51
+ expect(validator(null)).toBe(false);
52
+ });
53
+
54
+ test("literal validator", () => {
55
+ const stringLiteral = v.literal("success");
56
+ expect(stringLiteral("success")).toBe(true);
57
+ expect(stringLiteral("fail")).toBe(false);
58
+ expect(stringLiteral(null)).toBe(false);
59
+
60
+ const numberLiteral = v.literal(42);
61
+ expect(numberLiteral(42)).toBe(true);
62
+ expect(numberLiteral(43)).toBe(false);
63
+
64
+ const boolLiteral = v.literal(true);
65
+ expect(boolLiteral(true)).toBe(true);
66
+ expect(boolLiteral(false)).toBe(false);
67
+
68
+ const nullLiteral = v.literal(null);
69
+ expect(nullLiteral(null)).toBe(true);
70
+ expect(nullLiteral(undefined)).toBe(false);
71
+ });
72
+
73
+ test("obj validator", () => {
74
+ const validator = v.obj({
75
+ name: v.string(),
76
+ age: v.number(),
77
+ active: v.bool()
78
+ });
79
+
80
+ expect(validator({
81
+ name: "John",
82
+ age: 30,
83
+ active: true
84
+ })).toBe(true);
85
+
86
+ expect(validator({
87
+ name: "John",
88
+ age: "30",
89
+ active: true
90
+ })).toBe(false);
91
+
92
+ expect(validator({
93
+ name: "John",
94
+ age: 30
95
+ })).toBe(false);
96
+
97
+ expect(validator(null)).toBe(false);
98
+ expect(validator("not an object")).toBe(false);
99
+ });
100
+
101
+ test("complex nested validation", () => {
102
+ const validator = v.obj({
103
+ status: v.union(v.literal("success"), v.literal("error")),
104
+ data: v.optional(v.obj({
105
+ id: v.number(),
106
+ name: v.string()
107
+ })),
108
+ metadata: v.nullable(v.obj({
109
+ timestamp: v.number()
110
+ }))
111
+ });
112
+
113
+ expect(validator({
114
+ status: "success",
115
+ data: { id: 1, name: "test" },
116
+ metadata: { timestamp: 123456 }
117
+ })).toBe(true);
118
+
119
+ expect(validator({
120
+ status: "error",
121
+ data: undefined,
122
+ metadata: null
123
+ })).toBe(true);
124
+
125
+ expect(validator({
126
+ status: "pending",
127
+ data: undefined,
128
+ metadata: null
129
+ })).toBe(false);
130
+ });
131
+
132
+ test("array validator", () => {
133
+ const stringArrayValidator = v.array(v.string());
134
+ expect(stringArrayValidator(["hello", "world"])).toBe(true);
135
+ expect(stringArrayValidator([])).toBe(true);
136
+ expect(stringArrayValidator(["hello", 42])).toBe(false);
137
+ expect(stringArrayValidator("not an array")).toBe(false);
138
+ expect(stringArrayValidator(null)).toBe(false);
139
+
140
+ const numberArrayValidator = v.array(v.number());
141
+ expect(numberArrayValidator([1, 2, 3])).toBe(true);
142
+ expect(numberArrayValidator([1, "2", 3])).toBe(false);
143
+ });
144
+