Skip to content

Commit c8bd3a4

Browse files
authored
feat(context): scope context commands to active database (#98)
* feat(context): scope context commands to active database Context commands now call /v1/databases/{database_id}/context instead of /v1/context. A --database / -d flag is added to the context command; when omitted it falls back to the active database set via 'hotdata databases set'. Exits with a clear error if no database is active. Paired with hotdata-dev/runtimedb#474. * fix(context): correct --database-id flag name in error message * fix(datasets): add missing type discriminator to dataset source payloads The DatasetSource schema is a tagged union requiring a 'type' field. Without it the API rejects both create-from-query and create-from-saved-query with a deserialization error. --------- Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com>
1 parent bef1d29 commit c8bd3a4

4 files changed

Lines changed: 43 additions & 27 deletions

File tree

src/command.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,16 @@ pub enum Commands {
228228
command: Option<SandboxCommands>,
229229
},
230230

231-
/// Sync workspace text context with local Markdown (`./<NAME>.md` in the current directory)
231+
/// Sync database context with local Markdown (`./<NAME>.md` in the current directory)
232232
Context {
233233
/// Workspace ID (defaults to first workspace from login)
234234
#[arg(long, short = 'w', global = true)]
235235
workspace_id: Option<String>,
236236

237+
/// Database ID (defaults to active database set via 'hotdata databases set')
238+
#[arg(long, short = 'd', global = true)]
239+
database_id: Option<String>,
240+
237241
#[command(subcommand)]
238242
command: ContextCommands,
239243
},
@@ -852,7 +856,7 @@ pub enum ContextCommands {
852856
name: String,
853857
},
854858

855-
/// Download context from the workspace to ./<NAME>.md
859+
/// Download context from the database to ./<NAME>.md
856860
Pull {
857861
/// Context name (trailing `.md` ignored, e.g. `USER.md` → `USER`)
858862
name: String,
@@ -866,7 +870,7 @@ pub enum ContextCommands {
866870
dry_run: bool,
867871
},
868872

869-
/// Upload ./<NAME>.md to the workspace as named context
873+
/// Upload ./<NAME>.md to the database as named context
870874
Push {
871875
/// Context name (trailing `.md` ignored, e.g. `USER.md` → `USER`; reads `./USER.md`)
872876
name: String,

src/context.rs

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Workspace context: `/v1/context` sync with `./{NAME}.md` in the current directory.
1+
//! Database context: `/v1/databases/{id}/context` sync with `./{NAME}.md` in the current directory.
22
33
use crate::api::ApiClient;
44
use crossterm::style::Stylize;
@@ -28,25 +28,25 @@ static RESERVED_WORDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
2828
});
2929

3030
#[derive(Debug, Deserialize, Serialize)]
31-
struct WorkspaceContextEntry {
31+
struct DatabaseContextEntry {
3232
name: String,
3333
content: String,
3434
updated_at: String,
3535
}
3636

3737
#[derive(Deserialize)]
3838
struct ListResponse {
39-
contexts: Vec<WorkspaceContextEntry>,
39+
contexts: Vec<DatabaseContextEntry>,
4040
}
4141

4242
#[derive(Deserialize)]
4343
struct GetResponse {
44-
context: WorkspaceContextEntry,
44+
context: DatabaseContextEntry,
4545
}
4646

4747
#[derive(Deserialize)]
4848
struct UpsertResponse {
49-
context: WorkspaceContextEntry,
49+
context: DatabaseContextEntry,
5050
}
5151

5252
/// Normalizes a context name from the CLI: trims, takes the final path segment, and strips a
@@ -124,9 +124,10 @@ fn local_md_path(name: &str) -> PathBuf {
124124

125125
fn fetch_context(
126126
api: &ApiClient,
127+
database_id: &str,
127128
name: &str,
128-
) -> Result<WorkspaceContextEntry, reqwest::StatusCode> {
129-
let path = format!("/context/{name}");
129+
) -> Result<DatabaseContextEntry, reqwest::StatusCode> {
130+
let path = format!("/databases/{database_id}/context/{name}");
130131
let (status, body) = api.get_raw(&path);
131132
if status == reqwest::StatusCode::NOT_FOUND {
132133
return Err(status);
@@ -143,11 +144,11 @@ fn fetch_context(
143144
Ok(parsed.context)
144145
}
145146

146-
pub fn list(workspace_id: &str, prefix: Option<&str>, format: &str) {
147+
pub fn list(workspace_id: &str, database_id: &str, prefix: Option<&str>, format: &str) {
147148
let api = ApiClient::new(Some(workspace_id));
148-
let body: ListResponse = api.get("/context");
149+
let body: ListResponse = api.get(&format!("/databases/{database_id}/context"));
149150

150-
let mut rows: Vec<WorkspaceContextEntry> = body.contexts;
151+
let mut rows: Vec<DatabaseContextEntry> = body.contexts;
151152
if let Some(p) = prefix {
152153
rows.retain(|c| c.name.starts_with(p));
153154
}
@@ -176,15 +177,15 @@ pub fn list(workspace_id: &str, prefix: Option<&str>, format: &str) {
176177
}
177178
}
178179

179-
pub fn show(workspace_id: &str, name: &str) {
180+
pub fn show(workspace_id: &str, database_id: &str, name: &str) {
180181
let name = normalize_context_cli_name(name);
181182
if let Err(e) = validate_context_stem(&name) {
182183
eprintln!("error: {e}");
183184
std::process::exit(1);
184185
}
185186

186187
let api = ApiClient::new(Some(workspace_id));
187-
match fetch_context(&api, &name) {
188+
match fetch_context(&api, database_id, &name) {
188189
Ok(ctx) => {
189190
print!("{}", ctx.content);
190191
if !ctx.content.ends_with('\n') {
@@ -194,7 +195,7 @@ pub fn show(workspace_id: &str, name: &str) {
194195
Err(reqwest::StatusCode::NOT_FOUND) => {
195196
eprintln!(
196197
"{}",
197-
format!("error: no context named '{name}' in this workspace.").red()
198+
format!("error: no context named '{name}' in this database.").red()
198199
);
199200
eprintln!(
200201
"{}",
@@ -207,7 +208,7 @@ pub fn show(workspace_id: &str, name: &str) {
207208
}
208209
}
209210

210-
pub fn pull(workspace_id: &str, name: &str, force: bool, dry_run: bool) {
211+
pub fn pull(workspace_id: &str, database_id: &str, name: &str, force: bool, dry_run: bool) {
211212
let name = normalize_context_cli_name(name);
212213
if let Err(e) = validate_context_stem(&name) {
213214
eprintln!("error: {e}");
@@ -229,12 +230,12 @@ pub fn pull(workspace_id: &str, name: &str, force: bool, dry_run: bool) {
229230
}
230231

231232
let api = ApiClient::new(Some(workspace_id));
232-
let ctx = match fetch_context(&api, &name) {
233+
let ctx = match fetch_context(&api, database_id, &name) {
233234
Ok(c) => c,
234235
Err(reqwest::StatusCode::NOT_FOUND) => {
235236
eprintln!(
236237
"{}",
237-
format!("error: no context named '{name}' in this workspace.").red()
238+
format!("error: no context named '{name}' in this database.").red()
238239
);
239240
std::process::exit(1);
240241
}
@@ -270,7 +271,7 @@ pub fn pull(workspace_id: &str, name: &str, force: bool, dry_run: bool) {
270271
);
271272
}
272273

273-
pub fn push(workspace_id: &str, name: &str, dry_run: bool) {
274+
pub fn push(workspace_id: &str, database_id: &str, name: &str, dry_run: bool) {
274275
let name = normalize_context_cli_name(name);
275276
if let Err(e) = validate_context_stem(&name) {
276277
eprintln!("error: {e}");
@@ -310,7 +311,7 @@ pub fn push(workspace_id: &str, name: &str, dry_run: bool) {
310311

311312
let api = ApiClient::new(Some(workspace_id));
312313
let body = json!({ "name": &name, "content": content });
313-
let resp: UpsertResponse = api.post("/context", &body);
314+
let resp: UpsertResponse = api.post(&format!("/databases/{database_id}/context"), &body);
314315

315316
println!(
316317
"{}",

src/datasets.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ fn create_dataset(
107107

108108
pub fn create_from_query(workspace_id: &str, sql: &str, description: Option<&str>, name: &str) {
109109
let api = ApiClient::new(Some(workspace_id));
110-
create_dataset(&api, description, name, json!({ "sql": sql }));
110+
create_dataset(&api, description, name, json!({ "type": "sql_query", "sql": sql }));
111111
}
112112

113113
pub fn create_from_saved_query(
@@ -117,7 +117,7 @@ pub fn create_from_saved_query(
117117
name: &str,
118118
) {
119119
let api = ApiClient::new(Some(workspace_id));
120-
create_dataset(&api, description, name, json!({ "saved_query_id": query_id }));
120+
create_dataset(&api, description, name, json!({ "type": "saved_query", "saved_query_id": query_id }));
121121
}
122122

123123
pub fn list(workspace_id: &str, limit: Option<u32>, offset: Option<u32>, format: &str) {

src/main.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -939,21 +939,32 @@ fn main() {
939939
}
940940
Commands::Context {
941941
workspace_id,
942+
database_id,
942943
command,
943944
} => {
944945
let workspace_id = resolve_workspace(workspace_id);
946+
let database_id = database_id
947+
.or_else(|| config::load_current_database("default", &workspace_id))
948+
.unwrap_or_else(|| {
949+
eprintln!(
950+
"error: no active database. Use 'hotdata databases set <id>' to set one, or pass --database-id."
951+
);
952+
std::process::exit(1);
953+
});
945954
match command {
946955
ContextCommands::List { output, prefix } => {
947-
context::list(&workspace_id, prefix.as_deref(), &output)
956+
context::list(&workspace_id, &database_id, prefix.as_deref(), &output)
957+
}
958+
ContextCommands::Show { name } => {
959+
context::show(&workspace_id, &database_id, &name)
948960
}
949-
ContextCommands::Show { name } => context::show(&workspace_id, &name),
950961
ContextCommands::Pull {
951962
name,
952963
force,
953964
dry_run,
954-
} => context::pull(&workspace_id, &name, force, dry_run),
965+
} => context::pull(&workspace_id, &database_id, &name, force, dry_run),
955966
ContextCommands::Push { name, dry_run } => {
956-
context::push(&workspace_id, &name, dry_run)
967+
context::push(&workspace_id, &database_id, &name, dry_run)
957968
}
958969
}
959970
}

0 commit comments

Comments
 (0)