Skip to content

Commit e27ced1

Browse files
feat(database-ui): use zod for parsing responses
1 parent 3bacf53 commit e27ced1

File tree

4 files changed

+88
-26
lines changed

4 files changed

+88
-26
lines changed

packages/cli/src/db-studio/api/http/schemas.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { z } from "zod";
1+
import { z } from "zod/v3";
22

33
/**
44
* Foreign key reference information
@@ -125,6 +125,17 @@ export type Filter = {
125125
value?: string | number | null | string[] // undefined for is_null/is_not_null, array for 'in'
126126
}
127127

128+
/**
129+
* Filter schema for runtime validation
130+
*/
131+
export const FilterSchema = z.object({
132+
column: z.string().min(1, "Column name cannot be empty"),
133+
operator: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'like', 'not_like', 'in', 'is_null', 'is_not_null']),
134+
value: z.union([z.string(), z.number(), z.null(), z.array(z.string())]).optional(),
135+
});
136+
137+
export const FiltersArraySchema = z.array(FilterSchema);
138+
128139
/**
129140
* Delete rows request schema
130141
* Expects an array of primary key objects
@@ -145,3 +156,43 @@ export const DeleteRowsResponseSchema = z.object({
145156

146157
export type DeleteRowsResponse = z.infer<typeof DeleteRowsResponseSchema>;
147158

159+
/**
160+
* Execute query request schema
161+
*/
162+
export const ExecuteQueryRequestSchema = z.object({
163+
sql: z.string().min(1, "SQL query cannot be empty"),
164+
params: z.array(z.unknown()).optional(),
165+
});
166+
167+
export type ExecuteQueryRequest = z.infer<typeof ExecuteQueryRequestSchema>;
168+
169+
/**
170+
* Create table column schema
171+
*/
172+
export const CreateTableColumnSchema = z.object({
173+
name: z.string().min(1, "Column name cannot be empty"),
174+
type: z.string().min(1, "Column type cannot be empty"),
175+
constraints: z.string().optional(),
176+
});
177+
178+
/**
179+
* Create table request schema
180+
*/
181+
export const CreateTableRequestSchema = z.object({
182+
name: z.string().min(1, "Table name cannot be empty"),
183+
schema: z.array(CreateTableColumnSchema).min(1, "Schema must contain at least one column"),
184+
});
185+
186+
export type CreateTableRequest = z.infer<typeof CreateTableRequestSchema>;
187+
188+
/**
189+
* Drop table request schema
190+
*/
191+
export const DropTableRequestSchema = z.object({
192+
confirm: z.literal(true, {
193+
errorMap: () => ({ message: "Must set 'confirm: true' to drop table (safety check)" }),
194+
}),
195+
});
196+
197+
export type DropTableRequest = z.infer<typeof DropTableRequestSchema>;
198+

packages/cli/src/db-studio/api/routes/query.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@ import { jsonResponse, handleSQLError, errorResponse } from "../http/responses.t
22
import { withDatabase } from "../http/middleware.ts";
33
import { parseRequestBody } from "../utils/request.ts";
44
import { executeQuery } from "../database.ts";
5+
import { ExecuteQueryRequestSchema } from "../http/schemas.ts";
56

67
/**
78
* Execute a raw SQL query with rich metadata
89
*/
910
export const executeQueryRoute = withDatabase(async (context) => {
1011
const { db, body } = context;
1112
const data = parseRequestBody(body);
12-
const sql = data.sql as string | undefined;
13-
const queryParams = data.params as unknown[] | undefined;
13+
const result = ExecuteQueryRequestSchema.safeParse(data);
1414

15-
if (!sql) {
16-
return errorResponse("Request body must contain 'sql' string", 400);
15+
if (!result.success) {
16+
return errorResponse(
17+
`Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`,
18+
400
19+
);
1720
}
1821

22+
const { sql, params: queryParams } = result.data;
1923
const trimmedSql = sql.trim();
2024
const startTime = performance.now();
2125

packages/cli/src/db-studio/api/routes/rows.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ export const deleteRowsRoute = withDatabase(async (context) => {
1818
if (tableNameError) return tableNameError;
1919

2020
// Parse and validate request body
21-
const g = DeleteRowsRequestSchema.safeParse(body);
22-
console.log(g)
2321
const data = parseRequestBody(body);
24-
const primaryKeys = data.primaryKeys as Array<Record<string, unknown>> | undefined;
22+
const result = DeleteRowsRequestSchema.safeParse(data);
2523

26-
if (!primaryKeys || !Array.isArray(primaryKeys) || primaryKeys.length === 0) {
24+
if (!result.success) {
2725
return errorResponse(
28-
"Request body must contain 'primaryKeys' array with at least one primary key",
26+
`Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`,
2927
400
3028
);
3129
}
3230

31+
const { primaryKeys } = result.data;
32+
3333
try {
3434
const result = deleteRows(db, params.tableName, primaryKeys);
3535
return jsonResponse({

packages/cli/src/db-studio/api/routes/tables.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { requireTableName } from "../utils/validation.ts";
44
import { parseRequestBody, parseQueryParams } from "../utils/request.ts";
55
import { getTables, getTableData } from "../database.ts";
66
import type { Filter } from "../http/schemas.ts";
7+
import { CreateTableRequestSchema, DropTableRequestSchema, FiltersArraySchema } from "../http/schemas.ts";
78

89
/**
910
* Get list of tables in a database
@@ -61,14 +62,17 @@ export const getTableDataRoute = withDatabase(async (context) => {
6162
try {
6263
const decodedWhere = decodeURIComponent(whereParam);
6364
const parsedWhere = JSON.parse(decodedWhere);
64-
65-
// Validate that it's an array
66-
if (!Array.isArray(parsedWhere)) {
67-
return errorResponse("Invalid where parameter: must be a JSON array of filters", 400);
65+
66+
// Validate using zod schema
67+
const result = FiltersArraySchema.safeParse(parsedWhere);
68+
if (!result.success) {
69+
return errorResponse(
70+
`Invalid where parameter: ${result.error.errors.map(e => e.message).join(", ")}`,
71+
400
72+
);
6873
}
6974

70-
// Type cast - rely on compile-time types rather than runtime validation
71-
filters = parsedWhere as Filter[];
75+
filters = result.data;
7276
} catch (err) {
7377
const errorMessage = err instanceof Error ? err.message : "Unknown error";
7478
return errorResponse(
@@ -93,16 +97,16 @@ export const getTableDataRoute = withDatabase(async (context) => {
9397
export const createTableRoute = withDatabase(async (context) => {
9498
const { db, body } = context;
9599
const data = parseRequestBody(body);
96-
const tableName = data.name as string | undefined;
97-
const schema = data.schema as Array<{ name: string; type: string; constraints?: string }> | undefined;
100+
const result = CreateTableRequestSchema.safeParse(data);
98101

99-
if (!tableName) {
100-
return errorResponse("Request body must contain 'name' for the table", 400);
102+
if (!result.success) {
103+
return errorResponse(
104+
`Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`,
105+
400
106+
);
101107
}
102108

103-
if (!schema || !Array.isArray(schema) || schema.length === 0) {
104-
return errorResponse("Request body must contain 'schema' array with at least one column", 400);
105-
}
109+
const { name: tableName, schema } = result.data;
106110

107111
const columns = schema.map(col => {
108112
const constraints = col.constraints ? ` ${col.constraints}` : "";
@@ -129,10 +133,13 @@ export const dropTableRoute = withDatabase(async (context) => {
129133
if (tableNameError) return tableNameError;
130134

131135
const data = parseRequestBody(body);
132-
const confirm = data.confirm as boolean | undefined;
136+
const result = DropTableRequestSchema.safeParse(data);
133137

134-
if (!confirm) {
135-
return errorResponse("Must set 'confirm: true' in request body to drop table (safety check)", 400);
138+
if (!result.success) {
139+
return errorResponse(
140+
`Invalid request body: ${result.error.errors.map(e => e.message).join(", ")}`,
141+
400
142+
);
136143
}
137144

138145
const sql = `DROP TABLE "${params.tableName}"`;

0 commit comments

Comments
 (0)