Skip to content

Commit 0b09795

Browse files
committed
More strictly type JSON APIs.
1 parent 9c2e753 commit 0b09795

File tree

15 files changed

+161
-104
lines changed

15 files changed

+161
-104
lines changed

docs/src/content/docs/reference/benchmarks.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ alternatives such as SupaBase, PocketBase, and vanilla SQLite.
2222

2323
## Disclaimer
2424

25-
In general, benchmarks are tricky, both to do well and to interpret.
25+
Generally benchmarks are tricky, both to do well and to interpret.
2626
Benchmarks never show how fast something can theoretically go but merely how
2727
fast the author managed to make it go.
2828
Micro-benchmarks, especially, offer a selective key-hole insights, which may be
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
2+
import type { JsonValue } from "./serde_json/JsonValue";
3+
4+
export type InsertRowRequest = {
5+
/**
6+
* Row data, which is expected to be a map from column name to value.
7+
*/
8+
row: { [key in string]?: JsonValue }, };
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
2+
import type { JsonValue } from "./serde_json/JsonValue";
23

34
export type UpdateRowRequest = { primary_key_column: string, primary_key_value: Object,
45
/**
5-
* This is expected to be a map from column name to value.
6+
* Row data, which is expected to be a map from column name to value.
67
*
7-
* Note that using an array here wouldn't make sense. The map allows for sparseness and only
8-
* updating specific cells.
8+
* Note that the row is represented as a map to allow selective cells as opposed to
9+
* Vec<serde_json::Value>. Absence is different from setting a column to NULL.
910
*/
10-
row: { [key: string]: Object | undefined }, };
11+
row: { [key in string]?: JsonValue }, };
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
2+
3+
export type JsonValue = number | string | Array<JsonValue> | { [key in string]?: JsonValue };

trailbase-core/src/admin/rows/delete_rows.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,12 @@ mod tests {
102102
use trailbase_sqlite::query_one_row;
103103

104104
use super::*;
105-
use crate::admin::rows::insert_row::insert_row_handler;
105+
use crate::admin::rows::insert_row::insert_row;
106106
use crate::admin::rows::list_rows::list_rows_handler;
107107
use crate::admin::rows::update_row::{update_row_handler, UpdateRowRequest};
108108
use crate::admin::table::{create_table_handler, CreateTableRequest};
109109
use crate::app_state::*;
110+
use crate::records::test_utils::json_row_from_value;
110111
use crate::schema::{Column, ColumnDataType, ColumnOption, Table};
111112
use crate::util::{b64_to_uuid, uuid_to_b64};
112113

@@ -152,12 +153,13 @@ mod tests {
152153
.unwrap();
153154

154155
let insert = |value: &str| {
155-
insert_row_handler(
156-
State(state.clone()),
157-
Path(table_name.clone()),
158-
Json(serde_json::json!({
156+
insert_row(
157+
&state,
158+
table_name.clone(),
159+
json_row_from_value(serde_json::json!({
159160
"col0": value,
160-
})),
161+
}))
162+
.unwrap(),
161163
)
162164
};
163165

@@ -171,12 +173,12 @@ mod tests {
171173
};
172174

173175
let id0 = {
174-
let Json(row) = insert("row0").await.unwrap();
176+
let row = insert("row0").await.unwrap();
175177
assert_eq!(&row[1], "row0");
176178
get_id(row)
177179
};
178180
let id1 = {
179-
let Json(row) = insert("row1").await.unwrap();
181+
let row = insert("row1").await.unwrap();
180182
assert_eq!(&row[1], "row1");
181183
get_id(row)
182184
};
@@ -198,9 +200,10 @@ mod tests {
198200
Json(UpdateRowRequest {
199201
primary_key_column: pk_col.clone(),
200202
primary_key_value: serde_json::Value::String(uuid_to_b64(&id0)),
201-
row: serde_json::json!({
203+
row: json_row_from_value(serde_json::json!({
202204
"col0": updated_value.to_string(),
203-
}),
205+
}))
206+
.unwrap(),
204207
}),
205208
)
206209
.await

trailbase-core/src/admin/rows/insert_row.rs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,41 @@
11
use axum::extract::{Path, State};
22
use axum::Json;
3+
use serde::{Deserialize, Serialize};
4+
use ts_rs::TS;
35

46
use crate::admin::AdminError as Error;
57
use crate::app_state::AppState;
6-
use crate::records::json_to_sql::{InsertQueryBuilder, Params};
8+
use crate::records::json_to_sql::{InsertQueryBuilder, JsonRow, Params};
79
use crate::records::sql_to_json::row_to_json_array;
810

9-
type Row = Vec<serde_json::Value>;
11+
#[derive(Debug, Serialize, Deserialize, Default, TS)]
12+
#[ts(export)]
13+
pub struct InsertRowRequest {
14+
/// Row data, which is expected to be a map from column name to value.
15+
pub row: JsonRow,
16+
}
1017

1118
pub async fn insert_row_handler(
1219
State(state): State<AppState>,
1320
Path(table_name): Path<String>,
14-
Json(request): Json<serde_json::Value>,
15-
) -> Result<Json<Row>, Error> {
16-
let row = insert_row(&state, table_name, request).await?;
17-
return Ok(Json(row));
21+
Json(request): Json<InsertRowRequest>,
22+
) -> Result<(), Error> {
23+
let _row = insert_row(&state, table_name, request.row).await?;
24+
return Ok(());
1825
}
1926

20-
pub async fn insert_row(
27+
pub(crate) async fn insert_row(
2128
state: &AppState,
2229
table_name: String,
23-
value: serde_json::Value,
24-
) -> Result<Row, Error> {
30+
json_row: JsonRow,
31+
) -> Result<Vec<serde_json::Value>, Error> {
2532
let Some(table_metadata) = state.table_metadata().get(&table_name) else {
2633
return Err(Error::Precondition(format!("Table {table_name} not found")));
2734
};
2835

2936
let row = InsertQueryBuilder::run(
3037
state,
31-
Params::from(&table_metadata, value, None)?,
38+
Params::from(&table_metadata, json_row, None)?,
3239
None,
3340
Some("*"),
3441
)

trailbase-core/src/admin/rows/update_row.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use ts_rs::TS;
55

66
use crate::admin::AdminError as Error;
77
use crate::app_state::AppState;
8-
use crate::records::json_to_sql::{simple_json_value_to_param, Params, UpdateQueryBuilder};
8+
use crate::records::json_to_sql::{
9+
simple_json_value_to_param, JsonRow, Params, UpdateQueryBuilder,
10+
};
911

1012
#[derive(Debug, Serialize, Deserialize, Default, TS)]
1113
#[ts(export)]
@@ -15,12 +17,11 @@ pub struct UpdateRowRequest {
1517
#[ts(type = "Object")]
1618
pub primary_key_value: serde_json::Value,
1719

18-
/// This is expected to be a map from column name to value.
20+
/// Row data, which is expected to be a map from column name to value.
1921
///
20-
/// Note that using an array here wouldn't make sense. The map allows for sparseness and only
21-
/// updating specific cells.
22-
#[ts(type = "{ [key: string]: Object | undefined }")]
23-
pub row: serde_json::Value,
22+
/// Note that the row is represented as a map to allow selective cells as opposed to
23+
/// Vec<serde_json::Value>. Absence is different from setting a column to NULL.
24+
pub row: JsonRow,
2425
}
2526

2627
pub async fn update_row_handler(

trailbase-core/src/records/create_record.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use utoipa::{IntoParams, ToSchema};
77
use crate::app_state::AppState;
88
use crate::auth::user::User;
99
use crate::extract::Either;
10-
use crate::records::json_to_sql::{InsertQueryBuilder, LazyParams};
10+
use crate::records::json_to_sql::{InsertQueryBuilder, JsonRow, LazyParams};
1111
use crate::records::{Permission, RecordError};
1212
use crate::schema::ColumnDataType;
1313

@@ -27,6 +27,7 @@ pub struct CreateRecordResponse {
2727
post,
2828
path = "/:name",
2929
params(CreateRecordQuery),
30+
request_body = serde_json::Value,
3031
responses(
3132
(status = 200, description = "Record id of successful insertion.", body = CreateRecordResponse),
3233
)
@@ -36,7 +37,7 @@ pub async fn create_record_handler(
3637
Path(api_name): Path<String>,
3738
Query(create_record_query): Query<CreateRecordQuery>,
3839
user: Option<User>,
39-
either_request: Either<serde_json::Value>,
40+
either_request: Either<JsonRow>,
4041
) -> Result<Response, RecordError> {
4142
let Some(api) = state.lookup_record_api(&api_name) else {
4243
return Err(RecordError::ApiNotFound);
@@ -184,7 +185,7 @@ mod test {
184185
Path("messages_api".to_string()),
185186
Query(CreateRecordQuery::default()),
186187
User::from_auth_token(&state, &user_x_token.auth_token),
187-
Either::Json(json),
188+
Either::Json(json_row_from_value(json).unwrap()),
188189
)
189190
.await;
190191
assert!(response.is_ok(), "{response:?}");
@@ -202,7 +203,7 @@ mod test {
202203
Path("messages_api".to_string()),
203204
Query(CreateRecordQuery::default()),
204205
User::from_auth_token(&state, &user_x_token.auth_token),
205-
Either::Json(json),
206+
Either::Json(json_row_from_value(json).unwrap()),
206207
)
207208
.await;
208209
assert!(response.is_err(), "{response:?}");
@@ -219,7 +220,7 @@ mod test {
219220
Path("messages_api".to_string()),
220221
Query(CreateRecordQuery::default()),
221222
User::from_auth_token(&state, &user_y_token.auth_token),
222-
Either::Json(json),
223+
Either::Json(json_row_from_value(json).unwrap()),
223224
)
224225
.await;
225226
assert!(response.is_err(), "{response:?}");
@@ -275,7 +276,7 @@ mod test {
275276
Path("messages_api".to_string()),
276277
Query(CreateRecordQuery::default()),
277278
User::from_auth_token(&state, &user_x_token.auth_token),
278-
Either::Json(json),
279+
Either::Json(json_row_from_value(json).unwrap()),
279280
)
280281
.await;
281282
assert!(response.is_ok(), "{response:?}");

trailbase-core/src/records/delete_record.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ mod test {
165165
Path("messages_api".to_string()),
166166
Query(CreateRecordQuery::default()),
167167
User::from_auth_token(state, auth_token),
168-
Either::Json(create_json),
168+
Either::Json(json_row_from_value(create_json).unwrap()),
169169
)
170170
.await;
171171

trailbase-core/src/records/json_to_sql.rs

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ impl From<crate::records::files::FileError> for QueryError {
107107

108108
type FileMetadataContents = Vec<(FileUpload, Vec<u8>)>;
109109

110+
// JSON type use to represent rows. Note that we use a map to represent rows sparsely.
111+
pub type JsonRow = serde_json::Map<String, serde_json::Value>;
112+
110113
#[derive(Default)]
111114
pub struct Params {
112115
table_name: String,
@@ -149,19 +152,15 @@ impl Params {
149152
/// value itself in contrast to when the original request was an actual JSON request.
150153
pub fn from(
151154
metadata: &TableMetadata,
152-
json: serde_json::Value,
155+
json: JsonRow,
153156
multipart_files: Option<Vec<FileUploadInput>>,
154157
) -> Result<Self, ParamsError> {
155-
let serde_json::Value::Object(map) = json else {
156-
return Err(ParamsError::NotAnObject);
157-
};
158-
159158
let mut params = Params {
160159
table_name: metadata.name().to_string(),
161160
..Default::default()
162161
};
163162

164-
for (key, value) in map {
163+
for (key, value) in json {
165164
// We simply skip unknown columns, this could simply be malformed input or version skew. This
166165
// is similar in spirit to protobuf's unknown fields behavior.
167166
let Some((col, col_meta)) = Self::column_by_name(metadata, &key) else {
@@ -836,7 +835,7 @@ fn extract_params_and_files_from_json(
836835
/// streams at all.
837836
pub struct LazyParams<'a> {
838837
// Input
839-
request: serde_json::Value,
838+
json_row: JsonRow,
840839
metadata: &'a TableMetadata,
841840
multipart_files: Option<Vec<FileUploadInput>>,
842841

@@ -847,11 +846,11 @@ pub struct LazyParams<'a> {
847846
impl<'a> LazyParams<'a> {
848847
pub fn new(
849848
metadata: &'a TableMetadata,
850-
request: serde_json::Value,
849+
json_row: JsonRow,
851850
multipart_files: Option<Vec<FileUploadInput>>,
852851
) -> Self {
853852
LazyParams {
854-
request,
853+
json_row,
855854
metadata,
856855
multipart_files,
857856
params: None,
@@ -863,20 +862,20 @@ impl<'a> LazyParams<'a> {
863862
return params.as_ref().map_err(|err| err.clone());
864863
}
865864

866-
let request = std::mem::take(&mut self.request);
865+
let json_row = std::mem::take(&mut self.json_row);
867866
let multipart_files = std::mem::take(&mut self.multipart_files);
868867

869868
let params = self
870869
.params
871-
.insert(Params::from(self.metadata, request, multipart_files));
870+
.insert(Params::from(self.metadata, json_row, multipart_files));
872871
return params.as_ref().map_err(|err| err.clone());
873872
}
874873

875874
pub fn consume(self) -> Result<Params, ParamsError> {
876875
if let Some(params) = self.params {
877876
return params;
878877
}
879-
return Params::from(self.metadata, self.request, self.multipart_files);
878+
return Params::from(self.metadata, self.json_row, self.multipart_files);
880879
}
881880
}
882881

@@ -887,6 +886,7 @@ mod tests {
887886
use serde_json::json;
888887

889888
use super::*;
889+
use crate::records::test_utils::json_row_from_value;
890890
use crate::schema::Table;
891891
use crate::table_metadata::{sqlite3_parse_into_statement, TableMetadata};
892892
use crate::util::id_to_b64;
@@ -980,7 +980,11 @@ mod tests {
980980
"real": real,
981981
});
982982

983-
assert_params(Params::from(&metadata, value, None)?);
983+
assert_params(Params::from(
984+
&metadata,
985+
json_row_from_value(value).unwrap(),
986+
None,
987+
)?);
984988
}
985989

986990
{
@@ -993,7 +997,11 @@ mod tests {
993997
"real": "3",
994998
});
995999

996-
assert_params(Params::from(&metadata, value, None)?);
1000+
assert_params(Params::from(
1001+
&metadata,
1002+
json_row_from_value(value).unwrap(),
1003+
None,
1004+
)?);
9971005
}
9981006

9991007
{
@@ -1007,7 +1015,7 @@ mod tests {
10071015
"real": "3",
10081016
});
10091017

1010-
assert!(Params::from(&metadata, value, None).is_err());
1018+
assert!(Params::from(&metadata, json_row_from_value(value).unwrap(), None).is_err());
10111019

10121020
// Test that nested JSON object can be passed.
10131021
let value = json!({
@@ -1021,7 +1029,7 @@ mod tests {
10211029
"real": "3",
10221030
});
10231031

1024-
let params = Params::from(&metadata, value, None).unwrap();
1032+
let params = Params::from(&metadata, json_row_from_value(value).unwrap(), None).unwrap();
10251033
assert_params(params);
10261034
}
10271035

@@ -1034,7 +1042,7 @@ mod tests {
10341042
"real": "3",
10351043
});
10361044

1037-
assert!(Params::from(&metadata, value, None).is_err());
1045+
assert!(Params::from(&metadata, json_row_from_value(value).unwrap(), None).is_err());
10381046

10391047
// Test that nested JSON array can be passed.
10401048
let nested_json_blob: Vec<u8> = vec![65, 66, 67, 68];
@@ -1051,7 +1059,7 @@ mod tests {
10511059
"real": "3",
10521060
});
10531061

1054-
let params = Params::from(&metadata, value, None).unwrap();
1062+
let params = Params::from(&metadata, json_row_from_value(value).unwrap(), None).unwrap();
10551063

10561064
let json_col: Vec<libsql::Value> = params
10571065
.params

0 commit comments

Comments
 (0)