Skip to content

Commit c843aae

Browse files
committed
Improve params validation and minor tweaks
1 parent 2fda553 commit c843aae

File tree

10 files changed

+199
-35
lines changed

10 files changed

+199
-35
lines changed

examples/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ensureSymlinkedDataDirectorySync } from "./deps.ts";
2-
import { numberArrayValidator, respond } from "../server/mod.ts";
2+
import { respond } from "../server/mod.ts";
33
import { add, animalsMakeNoise, makeName } from "./methods.ts";
4+
import { numberArrayValidator } from "../helpers/server/validation.ts";
45

56
export const methods = {
67
add: {
File renamed without changes.

server/util.ts renamed to helpers/server/validation.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { v } from "./util_deps.ts";
2-
import { type JsonArray, type JsonObject, type JsonValue } from "../types.ts";
1+
import { v } from "./deps.ts";
2+
import {
3+
type JsonArray,
4+
type JsonObject,
5+
type JsonValue,
6+
} from "../../types.ts";
37

48
export type StringOrNull = string | null;
59
export type NumberOrNull = number | null;

server/creation.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isArray, isFunction, isString } from "./deps.ts";
1+
import { isArray, isDefined, isFunction, isString } from "./deps.ts";
22
import { callMethodInWorker, isMethodObjectWithWorkerUrl } from "./worker.ts";
33
import { internalErrorData, validationErrorData } from "./error_data.ts";
44
import { CustomError } from "./custom_error.ts";
@@ -8,7 +8,7 @@ import {
88
type RpcBatchResponse,
99
type RpcResponse,
1010
} from "../types.ts";
11-
import { type ValidationObject } from "./validation.ts";
11+
import { validateInput, type ValidationObject } from "./validation.ts";
1212
import { type Options } from "./response.ts";
1313
import { type Methods } from "./method.ts";
1414

@@ -40,12 +40,11 @@ export async function executeMethods(
4040
? { method: methodOrObject, validation: null }
4141
: methodOrObject;
4242
if (validation) {
43-
try {
44-
validation.parse(params);
45-
} catch (error) {
43+
const { error } = validateInput(validation)(params);
44+
if (error) {
4645
return {
4746
id: validationObject.id,
48-
data: error.data,
47+
data: error,
4948
isError: true,
5049
...validationErrorData,
5150
};
@@ -71,7 +70,11 @@ export async function executeMethods(
7170
code: error.code,
7271
message: error.message,
7372
id: validationObject.id,
74-
data: error.data,
73+
data: isDefined(error.data)
74+
? error.data
75+
: options.publicErrorStack
76+
? error.stack
77+
: undefined,
7578
isError: true,
7679
};
7780
}

server/deps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ export {
77
} from "https://dev.zaubrik.com/[email protected]/functions/mod.ts";
88
export {
99
isArray,
10+
isDefined,
1011
isFunction,
1112
isNotNull,
1213
isObject,
1314
isPresent,
1415
isString,
1516
isUrl,
1617
} from "https://dev.zaubrik.com/[email protected]/type.js";
18+
export { tryToParse } from "https://dev.zaubrik.com/[email protected]/encoding/json.js";
1719
export { type Type } from "https://deno.land/x/[email protected]/mod.ts";

server/error_data.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ export const invalidRequestErrorData = {
1616
/**
1717
* -32000 to -32099 Reserved for implementation-defined server-errors.
1818
*/
19-
export const authErrorData = { code: -32020, message: "Authorization error" };
19+
export const authErrorData = { code: -32020, message: "Failed authorization" };
2020
export const validationErrorData = {
2121
code: -32030,
22-
message: "Validation error",
22+
message: "Failed params validation",
2323
};
2424
export const formDataErrorData = {
2525
code: -32040,

server/mod.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export * from "./response.ts";
22
export * from "./method.ts";
3-
export * from "./util.ts";

server/response_test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { assertEquals, create } from "../test_deps.ts";
22
import { type JsonObject } from "../types.ts";
33
import { type Options, respond } from "./response.ts";
44
import { CustomError } from "./custom_error.ts";
5-
import { numberArrayValidator } from "../server/util.ts";
5+
import { numberArrayValidator } from "../helpers/server/validation.ts";
66

77
function createReq(str: string) {
88
return new Request("http://0.0.0.0:8000", { body: str, method: "POST" });
@@ -276,7 +276,7 @@ Deno.test("rpc call with failed validation", async function (): Promise<
276276
const sentToServer =
277277
'{"jsonrpc": "2.0", "method": "add", "params": [10, "invalid"], "id": 1}';
278278
const sentToClient =
279-
'{"jsonrpc":"2.0","error":{"code":-32030,"message":"Validation error"},"id":1}';
279+
'{"jsonrpc":"2.0","error":{"code":-32030,"message":"Failed params validation","data":{"ok":false,"code":"invalid_type","expected":["number"],"path":[1]}},"id":1}';
280280

281281
assertEquals(
282282
await (await respond(methods)(createReq(sentToServer))).text(),
@@ -302,7 +302,7 @@ Deno.test("rpc call with jwt", async function (): Promise<void> {
302302
const sentToServer = '{"jsonrpc": "2.0", "method": "login", "id": 3}';
303303
const sentToClient = '{"jsonrpc": "2.0", "result": "Bob", "id": 3}';
304304
const authorizationError =
305-
'{"jsonrpc": "2.0", "error": {"code": -32020, "message": "Authorization error"}, "id": 3}';
305+
'{"jsonrpc": "2.0", "error": {"code": -32020, "message": "Failed authorization"}, "id": 3}';
306306
const reqOne = createReq(sentToServer);
307307
reqOne.headers.append("Authorization", `Bearer ${jwt}`);
308308
assertEquals(

server/validation.ts

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { isFunction, isNotNull, isObject, isString } from "./deps.ts";
1+
import {
2+
isFunction,
3+
isNotNull,
4+
isObject,
5+
isString,
6+
tryToParse,
7+
type Type,
8+
} from "./deps.ts";
29
import {
310
invalidParamsErrorData,
411
invalidRequestErrorData,
@@ -55,28 +62,22 @@ function isRpcId(input: unknown): input is RpcId {
5562
}
5663
}
5764

58-
function tryToParse(json: string) {
59-
try {
60-
return [JSON.parse(json), null];
61-
} catch {
62-
return [null, {
63-
id: null,
64-
isError: true,
65-
...parseErrorData,
66-
}];
67-
}
68-
}
69-
7065
export function validateRequest(
7166
body: string,
7267
methods: Methods,
7368
): ValidationObject | ValidationObject[] {
74-
const [decodedBody, parsingError] = tryToParse(body);
75-
if (parsingError) return parsingError;
76-
if (Array.isArray(decodedBody) && decodedBody.length > 0) {
77-
return decodedBody.map((rpc) => validateRpcRequestObject(rpc, methods));
69+
const { value, error } = tryToParse(body);
70+
if (error) {
71+
return {
72+
id: null,
73+
isError: true,
74+
...parseErrorData,
75+
};
76+
}
77+
if (Array.isArray(value) && value.length > 0) {
78+
return value.map((rpc) => validateRpcRequestObject(rpc, methods));
7879
} else {
79-
return validateRpcRequestObject(decodedBody, methods);
80+
return validateRpcRequestObject(value, methods);
8081
}
8182
}
8283

@@ -129,3 +130,51 @@ export function validateRpcRequestObject(
129130
};
130131
}
131132
}
133+
134+
export interface IssueTree {
135+
ok: boolean;
136+
code: string;
137+
expected: string[];
138+
key?: string;
139+
tree?: IssueTree;
140+
path?: string[];
141+
}
142+
143+
export function extractErrorPath(issueTree: IssueTree) {
144+
const path = issueTree.path || [];
145+
if (issueTree.key !== undefined) {
146+
path.push(issueTree.key);
147+
}
148+
const tree = issueTree.tree;
149+
if (isObject(tree)) {
150+
return extractErrorPath({ ...tree, path });
151+
}
152+
return { ...issueTree, path };
153+
}
154+
155+
// deno-lint-ignore no-explicit-any
156+
export function validateInput(valitaObject: Type<any>) {
157+
return (input: unknown) => {
158+
try {
159+
const value = valitaObject.parse(input);
160+
return {
161+
value,
162+
kind: "success",
163+
};
164+
} catch (error) {
165+
const treeResult = extractErrorPath(
166+
// deno-lint-ignore no-explicit-any
167+
(error as any).issueTree as IssueTree,
168+
);
169+
return {
170+
error: treeResult as {
171+
ok: Required<IssueTree>["ok"];
172+
code: Required<IssueTree>["code"];
173+
expected: Required<IssueTree>["expected"];
174+
path: Required<IssueTree>["path"];
175+
},
176+
kind: "failure",
177+
};
178+
}
179+
};
180+
}

server/validation_test.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
1+
// deno-lint-ignore-file no-explicit-any
2+
import { type Type } from "./deps.ts";
13
import { assertEquals } from "../test_deps.ts";
2-
import { validateRequest } from "./validation.ts";
4+
import {
5+
extractErrorPath,
6+
type IssueTree,
7+
validateInput,
8+
validateRequest,
9+
} from "./validation.ts";
310

411
const methods = {
512
subtract: (a: number, b: number) => a - b,
613
};
714

15+
// Mock `Type` class for testing
16+
class TypeMock implements Type<any> {
17+
constructor(public parse: (input: any) => any) {}
18+
nullable: any = () => this;
19+
toTerminals: any = () => [];
20+
try: any = (input: unknown) => ({ success: true, value: this.parse(input) });
21+
name: any = "TypeMock";
22+
func: any = () => this;
23+
optional: any = () => this;
24+
default: any = () => this;
25+
assert: any = () => this;
26+
map: any = () => this;
27+
chain: any = () => this;
28+
}
29+
30+
// Test cases for `validateRequest`
831
Deno.test("validate request object", function (): void {
932
assertEquals(
1033
validateRequest(
@@ -37,3 +60,86 @@ Deno.test("validate request object", function (): void {
3760
},
3861
);
3962
});
63+
64+
// Test cases for `extractErrorPath`
65+
Deno.test("extractErrorPath - simple IssueTree", () => {
66+
const issueTree: IssueTree = {
67+
ok: false,
68+
code: "error_code",
69+
expected: ["string"],
70+
};
71+
72+
const result = extractErrorPath(issueTree);
73+
assertEquals(result.path, []);
74+
});
75+
76+
Deno.test("extractErrorPath - nested IssueTree", () => {
77+
const issueTree: IssueTree = {
78+
ok: false,
79+
code: "error_code",
80+
expected: ["string"],
81+
key: "root",
82+
tree: {
83+
ok: false,
84+
code: "nested_error_code",
85+
expected: ["number"],
86+
key: "nested",
87+
},
88+
};
89+
90+
const result = extractErrorPath(issueTree);
91+
assertEquals(result.path, ["root", "nested"]);
92+
});
93+
94+
Deno.test("extractErrorPath - with path", () => {
95+
const issueTree: IssueTree = {
96+
ok: false,
97+
code: "error_code",
98+
expected: ["string"],
99+
key: "root",
100+
path: ["existing_path"],
101+
tree: {
102+
ok: false,
103+
code: "nested_error_code",
104+
expected: ["number"],
105+
key: "nested",
106+
},
107+
};
108+
109+
const result = extractErrorPath(issueTree);
110+
assertEquals(result.path, ["existing_path", "root", "nested"]);
111+
});
112+
113+
// Test cases for `validateInput`
114+
Deno.test("validateInput - valid input", () => {
115+
const validation = new TypeMock((input) => input);
116+
const input = "valid_input";
117+
const validate = validateInput(validation);
118+
119+
const result = validate(input);
120+
assertEquals(result.kind, "success");
121+
assertEquals(result.value, input);
122+
});
123+
124+
Deno.test("validateInput - invalid input", () => {
125+
const validation = new TypeMock(() => {
126+
throw {
127+
issueTree: {
128+
ok: false,
129+
code: "error_code",
130+
expected: ["string"],
131+
key: "input",
132+
},
133+
};
134+
});
135+
const input = "invalid_input";
136+
const validate = validateInput(validation);
137+
138+
const result = validate(input);
139+
assertEquals(result.kind, "failure");
140+
if (result.error) {
141+
assertEquals(result.error.path, ["input"]);
142+
} else {
143+
throw new Error("Expected error to be defined");
144+
}
145+
});

0 commit comments

Comments
 (0)