entity_graph/commands/
init.rs1use std::path::PathBuf;
5
6use anyhow::{bail, Context, Result};
7use serde_json::json;
8
9#[derive(Debug, clap::Args)]
11pub struct InitArgs {
12 #[arg(short, long, default_value = ".")]
14 pub out: PathBuf,
15
16 #[arg(long, default_value_t = false)]
18 pub force: bool,
19}
20
21pub fn run(args: &InitArgs) -> Result<()> {
23 let dir = &args.out;
24 if !dir.exists() {
25 std::fs::create_dir_all(dir)
26 .with_context(|| format!("cannot create directory {}", dir.display()))?;
27 }
28
29 let dest = dir.join("schema.json");
30 if dest.exists() && !args.force {
31 bail!(
32 "{} already exists — pass --force to overwrite",
33 dest.display()
34 );
35 }
36
37 let schema = starter_schema();
38 let json = serde_json::to_string_pretty(&schema)
39 .context("failed to serialise starter schema")?;
40
41 std::fs::write(&dest, json)
42 .with_context(|| format!("cannot write {}", dest.display()))?;
43
44 println!("Created {}", dest.display());
45 println!();
46 println!("Next steps:");
47 println!(" 1. Edit schema.json to define your entities.");
48 println!(" 2. Run `entity-graph generate --target react --schema schema.json --out src/db`");
49
50 Ok(())
51}
52
53fn starter_schema() -> serde_json::Value {
55 json!({
56 "version": "1.0",
57 "entities": {
58 "user": {
59 "fields": {
60 "id": { "type": "uuid", "primary": true, "required": true },
61 "email": { "type": "string", "required": true, "unique": true },
62 "name": { "type": "string", "required": true },
63 "role": {
64 "type": "enum",
65 "required": false,
66 "values": ["admin", "member", "viewer"],
67 "default": "member"
68 },
69 "createdAt": { "type": "datetime", "required": false, "auto": true }
70 },
71 "relations": {
72 "posts": { "type": "hasMany", "target": "post", "foreignKey": "userId" }
73 }
74 },
75 "post": {
76 "fields": {
77 "id": { "type": "uuid", "primary": true, "required": true },
78 "title": { "type": "string", "required": true },
79 "body": { "type": "string", "required": false },
80 "published": { "type": "boolean", "required": false, "default": false },
81 "userId": { "type": "uuid", "required": true },
82 "createdAt": { "type": "datetime", "required": false, "auto": true }
83 },
84 "relations": {
85 "author": { "type": "belongsTo", "target": "user", "foreignKey": "userId" }
86 }
87 }
88 },
89 "config": {
90 "localFirst": { "engine": "pglite", "sync": "electric" },
91 "ai": { "mcp": true, "a2a": false }
92 }
93 })
94}
95
96#[cfg(test)]
99mod tests {
100 use super::*;
101 use tempfile::tempdir;
102
103 #[test]
104 fn creates_schema_json_in_temp_dir() {
105 let tmp = tempdir().unwrap();
106 let args = InitArgs { out: tmp.path().to_path_buf(), force: false };
107 run(&args).unwrap();
108
109 let dest = tmp.path().join("schema.json");
110 assert!(dest.exists(), "schema.json should have been created");
111
112 let content = std::fs::read_to_string(&dest).unwrap();
113 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
114 assert_eq!(parsed["version"], "1.0");
115 assert!(parsed["entities"]["user"].is_object());
116 assert!(parsed["entities"]["post"].is_object());
117 }
118
119 #[test]
120 fn refuses_to_overwrite_without_force() {
121 let tmp = tempdir().unwrap();
122 let args = InitArgs { out: tmp.path().to_path_buf(), force: false };
123 run(&args).unwrap();
124
125 let result = run(&args);
127 assert!(result.is_err(), "should have refused to overwrite");
128 let msg = result.unwrap_err().to_string();
129 assert!(msg.contains("already exists"), "error should mention already-exists: {msg}");
130 }
131
132 #[test]
133 fn force_flag_allows_overwrite() {
134 let tmp = tempdir().unwrap();
135 let first = InitArgs { out: tmp.path().to_path_buf(), force: false };
136 run(&first).unwrap();
137
138 let second = InitArgs { out: tmp.path().to_path_buf(), force: true };
139 run(&second).unwrap(); }
141
142 #[test]
143 fn creates_output_dir_if_absent() {
144 let tmp = tempdir().unwrap();
145 let nested = tmp.path().join("a/b/c");
146 let args = InitArgs { out: nested.clone(), force: false };
147 run(&args).unwrap();
148 assert!(nested.join("schema.json").exists());
149 }
150
151 #[test]
152 fn starter_schema_is_valid_json() {
153 let v = starter_schema();
154 let s = serde_json::to_string(&v).unwrap();
156 let _back: serde_json::Value = serde_json::from_str(&s).unwrap();
157 }
158}