1use std::{
7 fmt, fs,
8 path::{Path, PathBuf},
9 process::Command,
10 str::FromStr,
11};
12
13use anyhow::{Context, Result};
14use tracing::info;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Language {
19 Python,
21 TypeScript,
23 Rust,
25 Java,
27 Kotlin,
29 Go,
31 CSharp,
33 Swift,
35 Scala,
37}
38
39impl fmt::Display for Language {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::Python => write!(f, "python"),
43 Self::TypeScript => write!(f, "typescript"),
44 Self::Rust => write!(f, "rust"),
45 Self::Java => write!(f, "java"),
46 Self::Kotlin => write!(f, "kotlin"),
47 Self::Go => write!(f, "go"),
48 Self::CSharp => write!(f, "csharp"),
49 Self::Swift => write!(f, "swift"),
50 Self::Scala => write!(f, "scala"),
51 }
52 }
53}
54
55impl Language {
56 pub fn from_extension(ext: &str) -> Option<Self> {
58 match ext {
59 "py" => Some(Self::Python),
60 "ts" | "tsx" => Some(Self::TypeScript),
61 "rs" => Some(Self::Rust),
62 "java" => Some(Self::Java),
63 "kt" | "kts" => Some(Self::Kotlin),
64 "go" => Some(Self::Go),
65 "cs" => Some(Self::CSharp),
66 "swift" => Some(Self::Swift),
67 "scala" | "sc" => Some(Self::Scala),
68 _ => None,
69 }
70 }
71}
72
73impl FromStr for Language {
74 type Err = String;
75
76 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
77 match s.to_lowercase().as_str() {
78 "python" | "py" => Ok(Self::Python),
79 "typescript" | "ts" => Ok(Self::TypeScript),
80 "rust" | "rs" => Ok(Self::Rust),
81 "java" | "jav" => Ok(Self::Java),
82 "kotlin" | "kt" => Ok(Self::Kotlin),
83 "go" | "golang" => Ok(Self::Go),
84 "csharp" | "c#" | "cs" => Ok(Self::CSharp),
85 "swift" => Ok(Self::Swift),
86 "scala" | "sc" => Ok(Self::Scala),
87 other => Err(format!(
88 "Unknown language: {other}. Choose: python, typescript, rust, java, kotlin, go, csharp, swift, scala"
89 )),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum Database {
97 Postgres,
99 Mysql,
101 Sqlite,
103 SqlServer,
105}
106
107impl fmt::Display for Database {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 match self {
110 Self::Postgres => write!(f, "postgres"),
111 Self::Mysql => write!(f, "mysql"),
112 Self::Sqlite => write!(f, "sqlite"),
113 Self::SqlServer => write!(f, "sqlserver"),
114 }
115 }
116}
117
118impl FromStr for Database {
119 type Err = String;
120
121 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
122 match s.to_lowercase().as_str() {
123 "postgres" | "postgresql" | "pg" => Ok(Self::Postgres),
124 "mysql" => Ok(Self::Mysql),
125 "sqlite" => Ok(Self::Sqlite),
126 "sqlserver" | "mssql" => Ok(Self::SqlServer),
127 other => Err(format!(
128 "Unknown database: {other}. Choose: postgres, mysql, sqlite, sqlserver"
129 )),
130 }
131 }
132}
133
134impl Database {
136 const fn toml_target(self) -> &'static str {
137 match self {
138 Self::Postgres => "postgresql",
139 Self::Mysql => "mysql",
140 Self::Sqlite => "sqlite",
141 Self::SqlServer => "sqlserver",
142 }
143 }
144
145 fn default_url(self, project_name: &str) -> String {
146 match self {
147 Self::Postgres => format!("postgresql://localhost/{project_name}"),
148 Self::Mysql => format!("mysql://localhost/{project_name}"),
149 Self::Sqlite => format!("{project_name}.db"),
150 Self::SqlServer => format!("mssql://localhost/{project_name}"),
151 }
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum ProjectSize {
158 Xs,
160 S,
162 M,
164}
165
166impl FromStr for ProjectSize {
167 type Err = String;
168
169 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
170 match s.to_lowercase().as_str() {
171 "xs" => Ok(Self::Xs),
172 "s" => Ok(Self::S),
173 "m" => Ok(Self::M),
174 other => Err(format!("Unknown size: {other}. Choose: xs, s, m")),
175 }
176 }
177}
178
179pub struct InitConfig {
181 pub project_name: String,
183 pub language: Language,
185 pub database: Database,
187 pub size: ProjectSize,
189 pub no_git: bool,
191}
192
193pub fn run(config: &InitConfig) -> Result<()> {
195 let project_dir = PathBuf::from(&config.project_name);
196
197 if project_dir.exists() {
198 anyhow::bail!(
199 "Directory '{}' already exists. Choose a different name or remove it first.",
200 config.project_name
201 );
202 }
203
204 info!("Creating project: {}", config.project_name);
205 println!("Creating FraiseQL project: {}", config.project_name);
206
207 fs::create_dir_all(&project_dir)
209 .context(format!("Failed to create directory: {}", config.project_name))?;
210
211 create_gitignore(&project_dir)?;
213
214 create_toml_config(&project_dir, config)?;
216
217 create_schema_json(&project_dir)?;
219
220 create_db_structure(&project_dir, config)?;
222
223 create_authoring_skeleton(&project_dir, config)?;
225
226 if !config.no_git {
228 init_git(&project_dir)?;
229 }
230
231 println!();
232 println!("Project created at ./{}", config.project_name);
233 println!();
234 println!("Next steps:");
235 println!(" cd {}", config.project_name);
236 println!(" fraiseql compile fraiseql.toml");
237 if !config.no_git {
238 println!(" git add -A && git commit -m \"Initial FraiseQL project\"");
239 }
240 println!();
241
242 Ok(())
243}
244
245fn create_gitignore(project_dir: &Path) -> Result<()> {
246 let content = "\
247# FraiseQL compiled output
248schema.compiled.json
249
250# Rust
251target/
252
253# Python
254__pycache__/
255*.pyc
256.venv/
257
258# TypeScript / Node
259node_modules/
260dist/
261
262# IDE
263.idea/
264.vscode/
265*.swp
266*.swo
267
268# OS
269.DS_Store
270Thumbs.db
271
272# Environment
273.env
274.env.local
275";
276 fs::write(project_dir.join(".gitignore"), content).context("Failed to create .gitignore")?;
277 info!("Created .gitignore");
278 Ok(())
279}
280
281fn create_toml_config(project_dir: &Path, config: &InitConfig) -> Result<()> {
282 let db_url = config.database.default_url(&config.project_name);
283 let db_target = config.database.toml_target();
284
285 let content = format!(
286 r#"[project]
287name = "{name}"
288version = "0.1.0"
289description = "A FraiseQL project"
290database_target = "{db_target}"
291
292[fraiseql]
293schema_file = "schema.json"
294output_file = "schema.compiled.json"
295
296[fraiseql.security.rate_limiting]
297enabled = true
298auth_start_max_requests = 100
299auth_start_window_secs = 60
300
301[fraiseql.security.audit_logging]
302enabled = true
303log_level = "info"
304
305# Database connection URL — set via DATABASE_URL environment variable at runtime
306# {db_url}
307"#,
308 name = config.project_name,
309 );
310
311 fs::write(project_dir.join("fraiseql.toml"), content)
312 .context("Failed to create fraiseql.toml")?;
313 info!("Created fraiseql.toml");
314 Ok(())
315}
316
317fn create_schema_json(project_dir: &Path) -> Result<()> {
318 let schema = serde_json::json!({
321 "version": "2.0.0",
322 "types": [
323 {
324 "name": "Author",
325 "description": "Blog author",
326 "fields": [
327 { "name": "pk", "type": "Int", "nullable": false, "description": "Internal primary key" },
328 { "name": "id", "type": "ID", "nullable": false, "description": "Public UUID" },
329 { "name": "identifier", "type": "String", "nullable": false, "description": "URL slug" },
330 { "name": "name", "type": "String", "nullable": false },
331 { "name": "email", "type": "String", "nullable": false },
332 { "name": "bio", "type": "String", "nullable": true },
333 { "name": "created_at", "type": "DateTime", "nullable": false },
334 { "name": "updated_at", "type": "DateTime", "nullable": false }
335 ]
336 },
337 {
338 "name": "Post",
339 "description": "Blog post",
340 "fields": [
341 { "name": "pk", "type": "Int", "nullable": false },
342 { "name": "id", "type": "ID", "nullable": false },
343 { "name": "identifier", "type": "String", "nullable": false, "description": "URL slug" },
344 { "name": "title", "type": "String", "nullable": false },
345 { "name": "body", "type": "String", "nullable": false },
346 { "name": "published", "type": "Boolean", "nullable": false },
347 { "name": "author_id", "type": "ID", "nullable": false },
348 { "name": "created_at", "type": "DateTime", "nullable": false },
349 { "name": "updated_at", "type": "DateTime", "nullable": false }
350 ]
351 },
352 {
353 "name": "Comment",
354 "description": "Comment on a blog post",
355 "fields": [
356 { "name": "pk", "type": "Int", "nullable": false },
357 { "name": "id", "type": "ID", "nullable": false },
358 { "name": "body", "type": "String", "nullable": false },
359 { "name": "author_name", "type": "String", "nullable": false },
360 { "name": "post_id", "type": "ID", "nullable": false },
361 { "name": "created_at", "type": "DateTime", "nullable": false }
362 ]
363 },
364 {
365 "name": "Tag",
366 "description": "Categorization tag for posts",
367 "fields": [
368 { "name": "pk", "type": "Int", "nullable": false },
369 { "name": "id", "type": "ID", "nullable": false },
370 { "name": "identifier", "type": "String", "nullable": false, "description": "URL slug" },
371 { "name": "name", "type": "String", "nullable": false }
372 ]
373 }
374 ],
375 "queries": [
376 {
377 "name": "posts",
378 "return_type": "Post",
379 "return_array": true,
380 "sql_source": "v_post",
381 "description": "List all published posts"
382 },
383 {
384 "name": "post",
385 "return_type": "Post",
386 "return_array": false,
387 "sql_source": "v_post",
388 "args": [{ "name": "id", "type": "ID", "required": true }]
389 },
390 {
391 "name": "authors",
392 "return_type": "Author",
393 "return_array": true,
394 "sql_source": "v_author"
395 },
396 {
397 "name": "author",
398 "return_type": "Author",
399 "return_array": false,
400 "sql_source": "v_author",
401 "args": [{ "name": "id", "type": "ID", "required": true }]
402 },
403 {
404 "name": "tags",
405 "return_type": "Tag",
406 "return_array": true,
407 "sql_source": "v_tag"
408 }
409 ],
410 "mutations": [],
411 "enums": [],
412 "input_types": [],
413 "interfaces": [],
414 "unions": [],
415 "subscriptions": []
416 });
417
418 let content = serde_json::to_string_pretty(&schema).context("Failed to serialize schema")?;
419 fs::write(project_dir.join("schema.json"), content).context("Failed to create schema.json")?;
420 info!("Created schema.json");
421 Ok(())
422}
423
424fn create_db_structure(project_dir: &Path, config: &InitConfig) -> Result<()> {
425 match config.size {
426 ProjectSize::Xs => create_db_xs(project_dir, config),
427 ProjectSize::S => create_db_s(project_dir, config),
428 ProjectSize::M => create_db_m(project_dir, config),
429 }
430}
431
432fn create_db_xs(project_dir: &Path, config: &InitConfig) -> Result<()> {
433 let db_dir = project_dir.join("db").join("0_schema");
434 fs::create_dir_all(&db_dir).context("Failed to create db/0_schema")?;
435
436 let content = generate_single_schema_sql(config.database);
437 fs::write(db_dir.join("schema.sql"), content).context("Failed to create schema.sql")?;
438 info!("Created db/0_schema/schema.sql (xs layout)");
439 Ok(())
440}
441
442fn create_db_s(project_dir: &Path, config: &InitConfig) -> Result<()> {
443 let schema_dir = project_dir.join("db").join("0_schema");
444 let write_dir = schema_dir.join("01_write");
445 let read_dir = schema_dir.join("02_read");
446 let functions_dir = schema_dir.join("03_functions");
447
448 fs::create_dir_all(&write_dir).context("Failed to create 01_write")?;
449 fs::create_dir_all(&read_dir).context("Failed to create 02_read")?;
450 fs::create_dir_all(&functions_dir).context("Failed to create 03_functions")?;
451
452 let entities = ["author", "post", "comment", "tag"];
454 for (i, entity) in entities.iter().enumerate() {
455 let n = i + 1;
456 let (table_sql, view_sql, fn_sql) = generate_blog_entity_sql(config.database, entity);
457 fs::write(write_dir.join(format!("01{n}_tb_{entity}.sql")), table_sql)
458 .context(format!("Failed to create tb_{entity}.sql"))?;
459 if !view_sql.is_empty() {
460 fs::write(read_dir.join(format!("02{n}_v_{entity}.sql")), view_sql)
461 .context(format!("Failed to create v_{entity}.sql"))?;
462 }
463 if !fn_sql.is_empty() {
464 fs::write(functions_dir.join(format!("03{n}_fn_{entity}_crud.sql")), fn_sql)
465 .context(format!("Failed to create fn_{entity}_crud.sql"))?;
466 }
467 }
468
469 info!("Created db/0_schema/ (s layout)");
470 Ok(())
471}
472
473fn create_db_m(project_dir: &Path, config: &InitConfig) -> Result<()> {
474 let schema_dir = project_dir.join("db").join("0_schema");
475
476 let entities = ["author", "post", "comment", "tag"];
477 for entity in &entities {
478 let write_dir = schema_dir.join("01_write").join(entity);
479 let read_dir = schema_dir.join("02_read").join(entity);
480 let functions_dir = schema_dir.join("03_functions").join(entity);
481
482 fs::create_dir_all(&write_dir).context(format!("Failed to create 01_write/{entity}"))?;
483 fs::create_dir_all(&read_dir).context(format!("Failed to create 02_read/{entity}"))?;
484 fs::create_dir_all(&functions_dir)
485 .context(format!("Failed to create 03_functions/{entity}"))?;
486
487 let (table_sql, view_sql, fn_sql) = generate_blog_entity_sql(config.database, entity);
488 fs::write(write_dir.join(format!("tb_{entity}.sql")), table_sql)
489 .context(format!("Failed to create tb_{entity}.sql"))?;
490 if !view_sql.is_empty() {
491 fs::write(read_dir.join(format!("v_{entity}.sql")), view_sql)
492 .context(format!("Failed to create v_{entity}.sql"))?;
493 }
494 if !fn_sql.is_empty() {
495 fs::write(functions_dir.join(format!("fn_{entity}_crud.sql")), fn_sql)
496 .context(format!("Failed to create fn_{entity}_crud.sql"))?;
497 }
498 }
499
500 info!("Created db/0_schema/ (m layout)");
501 Ok(())
502}
503
504fn generate_single_schema_sql(database: Database) -> String {
506 match database {
507 Database::Postgres => BLOG_SCHEMA_POSTGRES.to_string(),
508 Database::Mysql => BLOG_SCHEMA_MYSQL.to_string(),
509 Database::Sqlite => BLOG_SCHEMA_SQLITE.to_string(),
510 Database::SqlServer => BLOG_SCHEMA_SQLSERVER.to_string(),
511 }
512}
513
514const BLOG_SCHEMA_POSTGRES: &str = "\
515-- FraiseQL Blog Schema
516-- Trinity pattern: pk (internal), id (public UUID), identifier (URL slug)
517
518-- Authors
519CREATE TABLE IF NOT EXISTS tb_author (
520 pk_author SERIAL PRIMARY KEY,
521 id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
522 identifier TEXT NOT NULL UNIQUE,
523 name TEXT NOT NULL,
524 email TEXT NOT NULL UNIQUE,
525 bio TEXT,
526 created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
527 updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
528);
529
530CREATE INDEX IF NOT EXISTS idx_tb_author_email ON tb_author (email);
531
532CREATE OR REPLACE VIEW v_author AS
533SELECT pk_author, id, identifier, name, email, bio, created_at, updated_at
534FROM tb_author;
535
536-- Posts
537CREATE TABLE IF NOT EXISTS tb_post (
538 pk_post SERIAL PRIMARY KEY,
539 id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
540 identifier TEXT NOT NULL UNIQUE,
541 title TEXT NOT NULL,
542 body TEXT NOT NULL,
543 published BOOLEAN NOT NULL DEFAULT false,
544 author_id UUID NOT NULL REFERENCES tb_author(id),
545 created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
546 updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
547);
548
549CREATE INDEX IF NOT EXISTS idx_tb_post_author ON tb_post (author_id);
550CREATE INDEX IF NOT EXISTS idx_tb_post_published ON tb_post (published) WHERE published = true;
551
552CREATE OR REPLACE VIEW v_post AS
553SELECT pk_post, id, identifier, title, body, published, author_id, created_at, updated_at
554FROM tb_post;
555
556-- Comments
557CREATE TABLE IF NOT EXISTS tb_comment (
558 pk_comment SERIAL PRIMARY KEY,
559 id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
560 body TEXT NOT NULL,
561 author_name TEXT NOT NULL,
562 post_id UUID NOT NULL REFERENCES tb_post(id) ON DELETE CASCADE,
563 created_at TIMESTAMPTZ NOT NULL DEFAULT now()
564);
565
566CREATE INDEX IF NOT EXISTS idx_tb_comment_post ON tb_comment (post_id);
567
568CREATE OR REPLACE VIEW v_comment AS
569SELECT pk_comment, id, body, author_name, post_id, created_at
570FROM tb_comment;
571
572-- Tags
573CREATE TABLE IF NOT EXISTS tb_tag (
574 pk_tag SERIAL PRIMARY KEY,
575 id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
576 identifier TEXT NOT NULL UNIQUE,
577 name TEXT NOT NULL UNIQUE
578);
579
580CREATE OR REPLACE VIEW v_tag AS
581SELECT pk_tag, id, identifier, name
582FROM tb_tag;
583
584-- Post-Tag junction
585CREATE TABLE IF NOT EXISTS tb_post_tag (
586 post_id UUID NOT NULL REFERENCES tb_post(id) ON DELETE CASCADE,
587 tag_id UUID NOT NULL REFERENCES tb_tag(id) ON DELETE CASCADE,
588 PRIMARY KEY (post_id, tag_id)
589);
590";
591
592const BLOG_SCHEMA_MYSQL: &str = "\
593-- FraiseQL Blog Schema
594-- Trinity pattern: pk (internal), id (public UUID), identifier (URL slug)
595
596CREATE TABLE IF NOT EXISTS tb_author (
597 pk_author INT AUTO_INCREMENT PRIMARY KEY,
598 id CHAR(36) NOT NULL DEFAULT (UUID()) UNIQUE,
599 identifier VARCHAR(255) NOT NULL UNIQUE,
600 name VARCHAR(255) NOT NULL,
601 email VARCHAR(255) NOT NULL UNIQUE,
602 bio TEXT,
603 created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
604 updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
605 INDEX idx_tb_author_email (email)
606);
607
608CREATE OR REPLACE VIEW v_author AS
609SELECT pk_author, id, identifier, name, email, bio, created_at, updated_at
610FROM tb_author;
611
612CREATE TABLE IF NOT EXISTS tb_post (
613 pk_post INT AUTO_INCREMENT PRIMARY KEY,
614 id CHAR(36) NOT NULL DEFAULT (UUID()) UNIQUE,
615 identifier VARCHAR(255) NOT NULL UNIQUE,
616 title VARCHAR(500) NOT NULL,
617 body LONGTEXT NOT NULL,
618 published BOOLEAN NOT NULL DEFAULT false,
619 author_id CHAR(36) NOT NULL,
620 created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
621 updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
622 INDEX idx_tb_post_author (author_id),
623 INDEX idx_tb_post_published (published)
624);
625
626CREATE OR REPLACE VIEW v_post AS
627SELECT pk_post, id, identifier, title, body, published, author_id, created_at, updated_at
628FROM tb_post;
629
630CREATE TABLE IF NOT EXISTS tb_comment (
631 pk_comment INT AUTO_INCREMENT PRIMARY KEY,
632 id CHAR(36) NOT NULL DEFAULT (UUID()) UNIQUE,
633 body TEXT NOT NULL,
634 author_name VARCHAR(255) NOT NULL,
635 post_id CHAR(36) NOT NULL,
636 created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
637 INDEX idx_tb_comment_post (post_id)
638);
639
640CREATE OR REPLACE VIEW v_comment AS
641SELECT pk_comment, id, body, author_name, post_id, created_at
642FROM tb_comment;
643
644CREATE TABLE IF NOT EXISTS tb_tag (
645 pk_tag INT AUTO_INCREMENT PRIMARY KEY,
646 id CHAR(36) NOT NULL DEFAULT (UUID()) UNIQUE,
647 identifier VARCHAR(255) NOT NULL UNIQUE,
648 name VARCHAR(255) NOT NULL UNIQUE
649);
650
651CREATE OR REPLACE VIEW v_tag AS
652SELECT pk_tag, id, identifier, name
653FROM tb_tag;
654";
655
656const BLOG_SCHEMA_SQLITE: &str = "\
657-- FraiseQL Blog Schema
658-- Trinity pattern: pk (internal), id (public UUID), identifier (URL slug)
659
660CREATE TABLE IF NOT EXISTS tb_author (
661 pk_author INTEGER PRIMARY KEY AUTOINCREMENT,
662 id TEXT NOT NULL UNIQUE,
663 identifier TEXT NOT NULL UNIQUE,
664 name TEXT NOT NULL,
665 email TEXT NOT NULL UNIQUE,
666 bio TEXT,
667 created_at TEXT NOT NULL DEFAULT (datetime('now')),
668 updated_at TEXT NOT NULL DEFAULT (datetime('now'))
669);
670
671CREATE VIEW IF NOT EXISTS v_author AS
672SELECT pk_author, id, identifier, name, email, bio, created_at, updated_at
673FROM tb_author;
674
675CREATE TABLE IF NOT EXISTS tb_post (
676 pk_post INTEGER PRIMARY KEY AUTOINCREMENT,
677 id TEXT NOT NULL UNIQUE,
678 identifier TEXT NOT NULL UNIQUE,
679 title TEXT NOT NULL,
680 body TEXT NOT NULL,
681 published INTEGER NOT NULL DEFAULT 0,
682 author_id TEXT NOT NULL REFERENCES tb_author(id),
683 created_at TEXT NOT NULL DEFAULT (datetime('now')),
684 updated_at TEXT NOT NULL DEFAULT (datetime('now'))
685);
686
687CREATE INDEX IF NOT EXISTS idx_tb_post_author ON tb_post (author_id);
688
689CREATE VIEW IF NOT EXISTS v_post AS
690SELECT pk_post, id, identifier, title, body, published, author_id, created_at, updated_at
691FROM tb_post;
692
693CREATE TABLE IF NOT EXISTS tb_comment (
694 pk_comment INTEGER PRIMARY KEY AUTOINCREMENT,
695 id TEXT NOT NULL UNIQUE,
696 body TEXT NOT NULL,
697 author_name TEXT NOT NULL,
698 post_id TEXT NOT NULL REFERENCES tb_post(id) ON DELETE CASCADE,
699 created_at TEXT NOT NULL DEFAULT (datetime('now'))
700);
701
702CREATE INDEX IF NOT EXISTS idx_tb_comment_post ON tb_comment (post_id);
703
704CREATE VIEW IF NOT EXISTS v_comment AS
705SELECT pk_comment, id, body, author_name, post_id, created_at
706FROM tb_comment;
707
708CREATE TABLE IF NOT EXISTS tb_tag (
709 pk_tag INTEGER PRIMARY KEY AUTOINCREMENT,
710 id TEXT NOT NULL UNIQUE,
711 identifier TEXT NOT NULL UNIQUE,
712 name TEXT NOT NULL UNIQUE
713);
714
715CREATE VIEW IF NOT EXISTS v_tag AS
716SELECT pk_tag, id, identifier, name
717FROM tb_tag;
718";
719
720const BLOG_SCHEMA_SQLSERVER: &str = "\
721-- FraiseQL Blog Schema
722-- Trinity pattern: pk (internal), id (public UUID), identifier (URL slug)
723
724IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='tb_author' AND xtype='U')
725CREATE TABLE tb_author (
726 pk_author INT IDENTITY(1,1) PRIMARY KEY,
727 id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID() UNIQUE,
728 identifier NVARCHAR(255) NOT NULL UNIQUE,
729 name NVARCHAR(255) NOT NULL,
730 email NVARCHAR(255) NOT NULL UNIQUE,
731 bio NVARCHAR(MAX),
732 created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
733 updated_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
734);
735GO
736
737CREATE OR ALTER VIEW v_author AS
738SELECT pk_author, id, identifier, name, email, bio, created_at, updated_at
739FROM tb_author;
740GO
741
742IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='tb_post' AND xtype='U')
743CREATE TABLE tb_post (
744 pk_post INT IDENTITY(1,1) PRIMARY KEY,
745 id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID() UNIQUE,
746 identifier NVARCHAR(255) NOT NULL UNIQUE,
747 title NVARCHAR(500) NOT NULL,
748 body NVARCHAR(MAX) NOT NULL,
749 published BIT NOT NULL DEFAULT 0,
750 author_id UNIQUEIDENTIFIER NOT NULL,
751 created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
752 updated_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
753);
754GO
755
756CREATE OR ALTER VIEW v_post AS
757SELECT pk_post, id, identifier, title, body, published, author_id, created_at, updated_at
758FROM tb_post;
759GO
760
761IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='tb_comment' AND xtype='U')
762CREATE TABLE tb_comment (
763 pk_comment INT IDENTITY(1,1) PRIMARY KEY,
764 id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID() UNIQUE,
765 body NVARCHAR(MAX) NOT NULL,
766 author_name NVARCHAR(255) NOT NULL,
767 post_id UNIQUEIDENTIFIER NOT NULL,
768 created_at DATETIME2 NOT NULL DEFAULT GETUTCDATE()
769);
770GO
771
772CREATE OR ALTER VIEW v_comment AS
773SELECT pk_comment, id, body, author_name, post_id, created_at
774FROM tb_comment;
775GO
776
777IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='tb_tag' AND xtype='U')
778CREATE TABLE tb_tag (
779 pk_tag INT IDENTITY(1,1) PRIMARY KEY,
780 id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID() UNIQUE,
781 identifier NVARCHAR(255) NOT NULL UNIQUE,
782 name NVARCHAR(255) NOT NULL UNIQUE
783);
784GO
785
786CREATE OR ALTER VIEW v_tag AS
787SELECT pk_tag, id, identifier, name
788FROM tb_tag;
789GO
790";
791
792fn generate_blog_entity_sql(database: Database, entity: &str) -> (String, String, String) {
794 if database != Database::Postgres {
795 if entity == "author" {
798 let single = generate_single_schema_sql(database);
799 return (single, String::new(), String::new());
800 }
801 return (
802 format!("-- See tb_author.sql for full {database} schema\n"),
803 String::new(),
804 String::new(),
805 );
806 }
807
808 match entity {
809 "author" => (
810 ENTITY_AUTHOR_TABLE.to_string(),
811 ENTITY_AUTHOR_VIEW.to_string(),
812 ENTITY_AUTHOR_FUNCTIONS.to_string(),
813 ),
814 "post" => (
815 ENTITY_POST_TABLE.to_string(),
816 ENTITY_POST_VIEW.to_string(),
817 ENTITY_POST_FUNCTIONS.to_string(),
818 ),
819 "comment" => (
820 ENTITY_COMMENT_TABLE.to_string(),
821 ENTITY_COMMENT_VIEW.to_string(),
822 ENTITY_COMMENT_FUNCTIONS.to_string(),
823 ),
824 "tag" => (
825 ENTITY_TAG_TABLE.to_string(),
826 ENTITY_TAG_VIEW.to_string(),
827 ENTITY_TAG_FUNCTIONS.to_string(),
828 ),
829 _ => (format!("-- Unknown entity: {entity}\n"), String::new(), String::new()),
830 }
831}
832
833const ENTITY_AUTHOR_TABLE: &str = "\
836-- Table: author
837-- Trinity pattern: pk (internal), id (public UUID), identifier (URL slug)
838
839CREATE TABLE IF NOT EXISTS tb_author (
840 pk_author SERIAL PRIMARY KEY,
841 id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
842 identifier TEXT NOT NULL UNIQUE,
843 name TEXT NOT NULL,
844 email TEXT NOT NULL UNIQUE,
845 bio TEXT,
846 created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
847 updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
848);
849
850CREATE INDEX IF NOT EXISTS idx_tb_author_email ON tb_author (email);
851";
852
853const ENTITY_AUTHOR_VIEW: &str = "\
854-- View: author (read-optimized)
855
856CREATE OR REPLACE VIEW v_author AS
857SELECT pk_author, id, identifier, name, email, bio, created_at, updated_at
858FROM tb_author;
859";
860
861const ENTITY_AUTHOR_FUNCTIONS: &str = "\
862-- CRUD functions for author
863
864CREATE OR REPLACE FUNCTION fn_author_create(
865 p_identifier TEXT,
866 p_name TEXT,
867 p_email TEXT,
868 p_bio TEXT DEFAULT NULL
869) RETURNS UUID
870LANGUAGE plpgsql AS $$
871DECLARE
872 v_id UUID;
873BEGIN
874 INSERT INTO tb_author (identifier, name, email, bio)
875 VALUES (p_identifier, p_name, p_email, p_bio)
876 RETURNING id INTO v_id;
877 RETURN v_id;
878END;
879$$;
880
881CREATE OR REPLACE FUNCTION fn_author_delete(p_id UUID)
882RETURNS BOOLEAN
883LANGUAGE plpgsql AS $$
884BEGIN
885 DELETE FROM tb_author WHERE id = p_id;
886 RETURN FOUND;
887END;
888$$;
889";
890
891const ENTITY_POST_TABLE: &str = "\
892-- Table: post
893-- Trinity pattern: pk (internal), id (public UUID), identifier (URL slug)
894
895CREATE TABLE IF NOT EXISTS tb_post (
896 pk_post SERIAL PRIMARY KEY,
897 id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
898 identifier TEXT NOT NULL UNIQUE,
899 title TEXT NOT NULL,
900 body TEXT NOT NULL,
901 published BOOLEAN NOT NULL DEFAULT false,
902 author_id UUID NOT NULL REFERENCES tb_author(id),
903 created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
904 updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
905);
906
907CREATE INDEX IF NOT EXISTS idx_tb_post_author ON tb_post (author_id);
908CREATE INDEX IF NOT EXISTS idx_tb_post_published ON tb_post (published) WHERE published = true;
909";
910
911const ENTITY_POST_VIEW: &str = "\
912-- View: post (read-optimized)
913
914CREATE OR REPLACE VIEW v_post AS
915SELECT pk_post, id, identifier, title, body, published, author_id, created_at, updated_at
916FROM tb_post;
917";
918
919const ENTITY_POST_FUNCTIONS: &str = "\
920-- CRUD functions for post
921
922CREATE OR REPLACE FUNCTION fn_post_create(
923 p_identifier TEXT,
924 p_title TEXT,
925 p_body TEXT,
926 p_author_id UUID
927) RETURNS UUID
928LANGUAGE plpgsql AS $$
929DECLARE
930 v_id UUID;
931BEGIN
932 INSERT INTO tb_post (identifier, title, body, author_id)
933 VALUES (p_identifier, p_title, p_body, p_author_id)
934 RETURNING id INTO v_id;
935 RETURN v_id;
936END;
937$$;
938
939CREATE OR REPLACE FUNCTION fn_post_publish(p_id UUID)
940RETURNS BOOLEAN
941LANGUAGE plpgsql AS $$
942BEGIN
943 UPDATE tb_post SET published = true, updated_at = now() WHERE id = p_id;
944 RETURN FOUND;
945END;
946$$;
947
948CREATE OR REPLACE FUNCTION fn_post_delete(p_id UUID)
949RETURNS BOOLEAN
950LANGUAGE plpgsql AS $$
951BEGIN
952 DELETE FROM tb_post WHERE id = p_id;
953 RETURN FOUND;
954END;
955$$;
956";
957
958const ENTITY_COMMENT_TABLE: &str = "\
959-- Table: comment
960
961CREATE TABLE IF NOT EXISTS tb_comment (
962 pk_comment SERIAL PRIMARY KEY,
963 id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
964 body TEXT NOT NULL,
965 author_name TEXT NOT NULL,
966 post_id UUID NOT NULL REFERENCES tb_post(id) ON DELETE CASCADE,
967 created_at TIMESTAMPTZ NOT NULL DEFAULT now()
968);
969
970CREATE INDEX IF NOT EXISTS idx_tb_comment_post ON tb_comment (post_id);
971";
972
973const ENTITY_COMMENT_VIEW: &str = "\
974-- View: comment (read-optimized)
975
976CREATE OR REPLACE VIEW v_comment AS
977SELECT pk_comment, id, body, author_name, post_id, created_at
978FROM tb_comment;
979";
980
981const ENTITY_COMMENT_FUNCTIONS: &str = "\
982-- CRUD functions for comment
983
984CREATE OR REPLACE FUNCTION fn_comment_create(
985 p_body TEXT,
986 p_author_name TEXT,
987 p_post_id UUID
988) RETURNS UUID
989LANGUAGE plpgsql AS $$
990DECLARE
991 v_id UUID;
992BEGIN
993 INSERT INTO tb_comment (body, author_name, post_id)
994 VALUES (p_body, p_author_name, p_post_id)
995 RETURNING id INTO v_id;
996 RETURN v_id;
997END;
998$$;
999
1000CREATE OR REPLACE FUNCTION fn_comment_delete(p_id UUID)
1001RETURNS BOOLEAN
1002LANGUAGE plpgsql AS $$
1003BEGIN
1004 DELETE FROM tb_comment WHERE id = p_id;
1005 RETURN FOUND;
1006END;
1007$$;
1008";
1009
1010const ENTITY_TAG_TABLE: &str = "\
1011-- Table: tag
1012-- Trinity pattern: pk (internal), id (public UUID), identifier (URL slug)
1013
1014CREATE TABLE IF NOT EXISTS tb_tag (
1015 pk_tag SERIAL PRIMARY KEY,
1016 id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
1017 identifier TEXT NOT NULL UNIQUE,
1018 name TEXT NOT NULL UNIQUE
1019);
1020";
1021
1022const ENTITY_TAG_VIEW: &str = "\
1023-- View: tag (read-optimized)
1024
1025CREATE OR REPLACE VIEW v_tag AS
1026SELECT pk_tag, id, identifier, name
1027FROM tb_tag;
1028";
1029
1030const ENTITY_TAG_FUNCTIONS: &str = "\
1031-- CRUD functions for tag
1032
1033CREATE OR REPLACE FUNCTION fn_tag_create(
1034 p_identifier TEXT,
1035 p_name TEXT
1036) RETURNS UUID
1037LANGUAGE plpgsql AS $$
1038DECLARE
1039 v_id UUID;
1040BEGIN
1041 INSERT INTO tb_tag (identifier, name)
1042 VALUES (p_identifier, p_name)
1043 RETURNING id INTO v_id;
1044 RETURN v_id;
1045END;
1046$$;
1047
1048CREATE OR REPLACE FUNCTION fn_tag_delete(p_id UUID)
1049RETURNS BOOLEAN
1050LANGUAGE plpgsql AS $$
1051BEGIN
1052 DELETE FROM tb_tag WHERE id = p_id;
1053 RETURN FOUND;
1054END;
1055$$;
1056";
1057
1058fn create_authoring_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1059 match config.language {
1060 Language::Python => create_python_skeleton(project_dir, config),
1061 Language::TypeScript => create_typescript_skeleton(project_dir, config),
1062 Language::Rust => create_rust_skeleton(project_dir, config),
1063 Language::Java => create_java_skeleton(project_dir, config),
1064 Language::Kotlin => create_kotlin_skeleton(project_dir, config),
1065 Language::Go => create_go_skeleton(project_dir, config),
1066 Language::CSharp => create_csharp_skeleton(project_dir, config),
1067 Language::Swift => create_swift_skeleton(project_dir, config),
1068 Language::Scala => create_scala_skeleton(project_dir, config),
1069 }
1070}
1071
1072fn create_python_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1073 let dir = project_dir.join("schema");
1074 fs::create_dir_all(&dir).context("Failed to create schema/ directory")?;
1075
1076 let content = format!(
1077 r#"""FraiseQL blog schema definition for {name}."""
1078
1079import fraiseql
1080
1081
1082@fraiseql.type(sql_source="v_author")
1083class Author:
1084 """Blog author with trinity pattern."""
1085
1086 pk: int
1087 id: ID
1088 identifier: str
1089 name: str
1090 email: str
1091 bio: str | None
1092 created_at: DateTime
1093 updated_at: DateTime
1094
1095
1096@fraiseql.type(sql_source="v_post")
1097class Post:
1098 """Blog post with trinity pattern."""
1099
1100 pk: int
1101 id: ID
1102 identifier: str
1103 title: str
1104 body: str
1105 published: bool
1106 author_id: ID
1107 created_at: DateTime
1108 updated_at: DateTime
1109
1110
1111@fraiseql.type(sql_source="v_comment")
1112class Comment:
1113 """Comment on a blog post."""
1114
1115 pk: int
1116 id: ID
1117 body: str
1118 author_name: str
1119 post_id: ID
1120 created_at: DateTime
1121
1122
1123@fraiseql.type(sql_source="v_tag")
1124class Tag:
1125 """Categorization tag for posts."""
1126
1127 pk: int
1128 id: ID
1129 identifier: str
1130 name: str
1131
1132
1133@fraiseql.query(return_type=Post, return_array=True, sql_source="v_post")
1134def posts() -> list[Post]:
1135 """List all published posts."""
1136 ...
1137
1138
1139@fraiseql.query(return_type=Post, sql_source="v_post")
1140def post(*, id: ID) -> Post:
1141 """Get post by ID."""
1142 ...
1143
1144
1145@fraiseql.query(return_type=Author, return_array=True, sql_source="v_author")
1146def authors() -> list[Author]:
1147 """List all authors."""
1148 ...
1149
1150
1151@fraiseql.query(return_type=Author, sql_source="v_author")
1152def author(*, id: ID) -> Author:
1153 """Get author by ID."""
1154 ...
1155
1156
1157@fraiseql.query(return_type=Tag, return_array=True, sql_source="v_tag")
1158def tags() -> list[Tag]:
1159 """List all tags."""
1160 ...
1161"#,
1162 name = config.project_name,
1163 );
1164
1165 fs::write(dir.join("schema.py"), content).context("Failed to create schema.py")?;
1166 info!("Created schema/schema.py");
1167 Ok(())
1168}
1169
1170fn create_typescript_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1171 let dir = project_dir.join("schema");
1172 fs::create_dir_all(&dir).context("Failed to create schema/ directory")?;
1173
1174 let content = format!(
1175 r#"/**
1176 * FraiseQL blog schema definition for {name}.
1177 */
1178
1179import {{ type_, query }} from "fraiseql";
1180
1181export const Author = type_("Author", {{
1182 sqlSource: "v_author",
1183 fields: {{
1184 pk: {{ type: "Int", nullable: false }},
1185 id: {{ type: "ID", nullable: false }},
1186 identifier: {{ type: "String", nullable: false }},
1187 name: {{ type: "String", nullable: false }},
1188 email: {{ type: "String", nullable: false }},
1189 bio: {{ type: "String", nullable: true }},
1190 created_at: {{ type: "DateTime", nullable: false }},
1191 updated_at: {{ type: "DateTime", nullable: false }},
1192 }},
1193}});
1194
1195export const Post = type_("Post", {{
1196 sqlSource: "v_post",
1197 fields: {{
1198 pk: {{ type: "Int", nullable: false }},
1199 id: {{ type: "ID", nullable: false }},
1200 identifier: {{ type: "String", nullable: false }},
1201 title: {{ type: "String", nullable: false }},
1202 body: {{ type: "String", nullable: false }},
1203 published: {{ type: "Boolean", nullable: false }},
1204 author_id: {{ type: "ID", nullable: false }},
1205 created_at: {{ type: "DateTime", nullable: false }},
1206 updated_at: {{ type: "DateTime", nullable: false }},
1207 }},
1208}});
1209
1210export const Comment = type_("Comment", {{
1211 sqlSource: "v_comment",
1212 fields: {{
1213 pk: {{ type: "Int", nullable: false }},
1214 id: {{ type: "ID", nullable: false }},
1215 body: {{ type: "String", nullable: false }},
1216 author_name: {{ type: "String", nullable: false }},
1217 post_id: {{ type: "ID", nullable: false }},
1218 created_at: {{ type: "DateTime", nullable: false }},
1219 }},
1220}});
1221
1222export const Tag = type_("Tag", {{
1223 sqlSource: "v_tag",
1224 fields: {{
1225 pk: {{ type: "Int", nullable: false }},
1226 id: {{ type: "ID", nullable: false }},
1227 identifier: {{ type: "String", nullable: false }},
1228 name: {{ type: "String", nullable: false }},
1229 }},
1230}});
1231
1232export const posts = query("posts", {{
1233 returnType: "Post",
1234 returnArray: true,
1235 sqlSource: "v_post",
1236}});
1237
1238export const post = query("post", {{
1239 returnType: "Post",
1240 returnArray: false,
1241 sqlSource: "v_post",
1242 args: [{{ name: "id", type: "ID", required: true }}],
1243}});
1244
1245export const authors = query("authors", {{
1246 returnType: "Author",
1247 returnArray: true,
1248 sqlSource: "v_author",
1249}});
1250
1251export const author = query("author", {{
1252 returnType: "Author",
1253 returnArray: false,
1254 sqlSource: "v_author",
1255 args: [{{ name: "id", type: "ID", required: true }}],
1256}});
1257
1258export const tagsQuery = query("tags", {{
1259 returnType: "Tag",
1260 returnArray: true,
1261 sqlSource: "v_tag",
1262}});
1263"#,
1264 name = config.project_name,
1265 );
1266
1267 fs::write(dir.join("schema.ts"), content).context("Failed to create schema.ts")?;
1268 info!("Created schema/schema.ts");
1269 Ok(())
1270}
1271
1272fn create_rust_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1273 let dir = project_dir.join("schema");
1274 fs::create_dir_all(&dir).context("Failed to create schema/ directory")?;
1275
1276 let content = format!(
1277 r#"//! FraiseQL blog schema definition for {name}.
1278
1279use fraiseql::{{type_, query}};
1280
1281/// Blog author with trinity pattern.
1282#[type_(sql_source = "v_author")]
1283pub struct Author {{
1284 pub pk: i32,
1285 pub id: ID,
1286 pub identifier: String,
1287 pub name: String,
1288 pub email: String,
1289 pub bio: Option<String>,
1290 pub created_at: DateTime,
1291 pub updated_at: DateTime,
1292}}
1293
1294/// Blog post with trinity pattern.
1295#[type_(sql_source = "v_post")]
1296pub struct Post {{
1297 pub pk: i32,
1298 pub id: ID,
1299 pub identifier: String,
1300 pub title: String,
1301 pub body: String,
1302 pub published: bool,
1303 pub author_id: ID,
1304 pub created_at: DateTime,
1305 pub updated_at: DateTime,
1306}}
1307
1308/// Comment on a blog post.
1309#[type_(sql_source = "v_comment")]
1310pub struct Comment {{
1311 pub pk: i32,
1312 pub id: ID,
1313 pub body: String,
1314 pub author_name: String,
1315 pub post_id: ID,
1316 pub created_at: DateTime,
1317}}
1318
1319/// Categorization tag for posts.
1320#[type_(sql_source = "v_tag")]
1321pub struct Tag {{
1322 pub pk: i32,
1323 pub id: ID,
1324 pub identifier: String,
1325 pub name: String,
1326}}
1327
1328#[query(return_type = "Post", return_array = true, sql_source = "v_post")]
1329pub fn posts() -> Vec<Post> {{
1330 unimplemented!("Schema definition only")
1331}}
1332
1333#[query(return_type = "Post", sql_source = "v_post")]
1334pub fn post(id: ID) -> Post {{
1335 unimplemented!("Schema definition only")
1336}}
1337
1338#[query(return_type = "Author", return_array = true, sql_source = "v_author")]
1339pub fn authors() -> Vec<Author> {{
1340 unimplemented!("Schema definition only")
1341}}
1342
1343#[query(return_type = "Author", sql_source = "v_author")]
1344pub fn author(id: ID) -> Author {{
1345 unimplemented!("Schema definition only")
1346}}
1347
1348#[query(return_type = "Tag", return_array = true, sql_source = "v_tag")]
1349pub fn tags() -> Vec<Tag> {{
1350 unimplemented!("Schema definition only")
1351}}
1352"#,
1353 name = config.project_name,
1354 );
1355
1356 fs::write(dir.join("schema.rs"), content).context("Failed to create schema.rs")?;
1357 info!("Created schema/schema.rs");
1358 Ok(())
1359}
1360
1361fn create_java_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1362 let dir = project_dir.join("schema");
1363 fs::create_dir_all(&dir).context("Failed to create schema/ directory")?;
1364
1365 let content = format!(
1366 r#"// FraiseQL blog schema definition for {name}.
1367
1368package schema;
1369
1370import fraiseql.FraiseQL;
1371import fraiseql.annotations.*;
1372
1373/// Blog author with trinity pattern.
1374@Type(sqlSource = "v_author")
1375public record Author(
1376 int pk,
1377 ID id,
1378 String identifier,
1379 String name,
1380 String email,
1381 @Nullable String bio,
1382 DateTime createdAt,
1383 DateTime updatedAt
1384) {{}}
1385
1386/// Blog post with trinity pattern.
1387@Type(sqlSource = "v_post")
1388public record Post(
1389 int pk,
1390 ID id,
1391 String identifier,
1392 String title,
1393 String body,
1394 boolean published,
1395 ID authorId,
1396 DateTime createdAt,
1397 DateTime updatedAt
1398) {{}}
1399
1400/// Comment on a blog post.
1401@Type(sqlSource = "v_comment")
1402public record Comment(
1403 int pk,
1404 ID id,
1405 String body,
1406 String authorName,
1407 ID postId,
1408 DateTime createdAt
1409) {{}}
1410
1411/// Categorization tag for posts.
1412@Type(sqlSource = "v_tag")
1413public record Tag(
1414 int pk,
1415 ID id,
1416 String identifier,
1417 String name
1418) {{}}
1419
1420@Query(returnType = Post.class, returnArray = true, sqlSource = "v_post")
1421public interface Posts {{}}
1422
1423@Query(returnType = Post.class, sqlSource = "v_post", args = @Arg(name = "id", type = "ID", required = true))
1424public interface PostById {{}}
1425
1426@Query(returnType = Author.class, returnArray = true, sqlSource = "v_author")
1427public interface Authors {{}}
1428
1429@Query(returnType = Author.class, sqlSource = "v_author", args = @Arg(name = "id", type = "ID", required = true))
1430public interface AuthorById {{}}
1431
1432@Query(returnType = Tag.class, returnArray = true, sqlSource = "v_tag")
1433public interface Tags {{}}
1434"#,
1435 name = config.project_name,
1436 );
1437
1438 fs::write(dir.join("schema.java"), content).context("Failed to create schema.java")?;
1439 info!("Created schema/schema.java");
1440 Ok(())
1441}
1442
1443fn create_kotlin_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1444 let dir = project_dir.join("schema");
1445 fs::create_dir_all(&dir).context("Failed to create schema/ directory")?;
1446
1447 let content = format!(
1448 r#"// FraiseQL blog schema definition for {name}.
1449
1450package schema
1451
1452import fraiseql.*
1453
1454/// Blog author with trinity pattern.
1455@Type(sqlSource = "v_author")
1456data class Author(
1457 val pk: Int,
1458 val id: ID,
1459 val identifier: String,
1460 val name: String,
1461 val email: String,
1462 val bio: String?,
1463 val createdAt: DateTime,
1464 val updatedAt: DateTime,
1465)
1466
1467/// Blog post with trinity pattern.
1468@Type(sqlSource = "v_post")
1469data class Post(
1470 val pk: Int,
1471 val id: ID,
1472 val identifier: String,
1473 val title: String,
1474 val body: String,
1475 val published: Boolean,
1476 val authorId: ID,
1477 val createdAt: DateTime,
1478 val updatedAt: DateTime,
1479)
1480
1481/// Comment on a blog post.
1482@Type(sqlSource = "v_comment")
1483data class Comment(
1484 val pk: Int,
1485 val id: ID,
1486 val body: String,
1487 val authorName: String,
1488 val postId: ID,
1489 val createdAt: DateTime,
1490)
1491
1492/// Categorization tag for posts.
1493@Type(sqlSource = "v_tag")
1494data class Tag(
1495 val pk: Int,
1496 val id: ID,
1497 val identifier: String,
1498 val name: String,
1499)
1500
1501@Query(returnType = Post::class, returnArray = true, sqlSource = "v_post")
1502fun posts(): List<Post> = TODO("Schema definition only")
1503
1504@Query(returnType = Post::class, sqlSource = "v_post")
1505fun post(id: ID): Post = TODO("Schema definition only")
1506
1507@Query(returnType = Author::class, returnArray = true, sqlSource = "v_author")
1508fun authors(): List<Author> = TODO("Schema definition only")
1509
1510@Query(returnType = Author::class, sqlSource = "v_author")
1511fun author(id: ID): Author = TODO("Schema definition only")
1512
1513@Query(returnType = Tag::class, returnArray = true, sqlSource = "v_tag")
1514fun tags(): List<Tag> = TODO("Schema definition only")
1515"#,
1516 name = config.project_name,
1517 );
1518
1519 fs::write(dir.join("schema.kt"), content).context("Failed to create schema.kt")?;
1520 info!("Created schema/schema.kt");
1521 Ok(())
1522}
1523
1524fn create_go_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1525 let dir = project_dir.join("schema");
1526 fs::create_dir_all(&dir).context("Failed to create schema/ directory")?;
1527
1528 let content = format!(
1529 r#"// FraiseQL blog schema definition for {name}.
1530
1531package schema
1532
1533import "fraiseql"
1534
1535// Author - Blog author with trinity pattern.
1536// @Type(sqlSource = "v_author")
1537type Author struct {{
1538 PK int `fraiseql:"pk"`
1539 ID ID `fraiseql:"id"`
1540 Identifier string `fraiseql:"identifier"`
1541 Name string `fraiseql:"name"`
1542 Email string `fraiseql:"email"`
1543 Bio *string `fraiseql:"bio"`
1544 CreatedAt DateTime `fraiseql:"created_at"`
1545 UpdatedAt DateTime `fraiseql:"updated_at"`
1546}}
1547
1548// Post - Blog post with trinity pattern.
1549// @Type(sqlSource = "v_post")
1550type Post struct {{
1551 PK int `fraiseql:"pk"`
1552 ID ID `fraiseql:"id"`
1553 Identifier string `fraiseql:"identifier"`
1554 Title string `fraiseql:"title"`
1555 Body string `fraiseql:"body"`
1556 Published bool `fraiseql:"published"`
1557 AuthorID ID `fraiseql:"author_id"`
1558 CreatedAt DateTime `fraiseql:"created_at"`
1559 UpdatedAt DateTime `fraiseql:"updated_at"`
1560}}
1561
1562// Comment - Comment on a blog post.
1563// @Type(sqlSource = "v_comment")
1564type Comment struct {{
1565 PK int `fraiseql:"pk"`
1566 ID ID `fraiseql:"id"`
1567 Body string `fraiseql:"body"`
1568 AuthorName string `fraiseql:"author_name"`
1569 PostID ID `fraiseql:"post_id"`
1570 CreatedAt DateTime `fraiseql:"created_at"`
1571}}
1572
1573// Tag - Categorization tag for posts.
1574// @Type(sqlSource = "v_tag")
1575type Tag struct {{
1576 PK int `fraiseql:"pk"`
1577 ID ID `fraiseql:"id"`
1578 Identifier string `fraiseql:"identifier"`
1579 Name string `fraiseql:"name"`
1580}}
1581
1582// Queries are registered via fraiseql.RegisterQuery().
1583func init() {{
1584 fraiseql.RegisterQuery("posts", fraiseql.QueryDef{{ReturnType: "Post", ReturnArray: true, SQLSource: "v_post"}})
1585 fraiseql.RegisterQuery("post", fraiseql.QueryDef{{ReturnType: "Post", SQLSource: "v_post", Args: []fraiseql.Arg{{{{Name: "id", Type: "ID", Required: true}}}}}})
1586 fraiseql.RegisterQuery("authors", fraiseql.QueryDef{{ReturnType: "Author", ReturnArray: true, SQLSource: "v_author"}})
1587 fraiseql.RegisterQuery("author", fraiseql.QueryDef{{ReturnType: "Author", SQLSource: "v_author", Args: []fraiseql.Arg{{{{Name: "id", Type: "ID", Required: true}}}}}})
1588 fraiseql.RegisterQuery("tags", fraiseql.QueryDef{{ReturnType: "Tag", ReturnArray: true, SQLSource: "v_tag"}})
1589}}
1590"#,
1591 name = config.project_name,
1592 );
1593
1594 fs::write(dir.join("schema.go"), content).context("Failed to create schema.go")?;
1595 info!("Created schema/schema.go");
1596 Ok(())
1597}
1598
1599fn create_csharp_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1600 let dir = project_dir.join("schema");
1601 fs::create_dir_all(&dir).context("Failed to create schema/ directory")?;
1602
1603 let content = format!(
1604 r#"// FraiseQL blog schema definition for {name}.
1605
1606using FraiseQL;
1607
1608namespace Schema;
1609
1610/// Blog author with trinity pattern.
1611[Type(SqlSource = "v_author")]
1612public record Author(
1613 int Pk,
1614 ID Id,
1615 string Identifier,
1616 string Name,
1617 string Email,
1618 string? Bio,
1619 DateTime CreatedAt,
1620 DateTime UpdatedAt
1621);
1622
1623/// Blog post with trinity pattern.
1624[Type(SqlSource = "v_post")]
1625public record Post(
1626 int Pk,
1627 ID Id,
1628 string Identifier,
1629 string Title,
1630 string Body,
1631 bool Published,
1632 ID AuthorId,
1633 DateTime CreatedAt,
1634 DateTime UpdatedAt
1635);
1636
1637/// Comment on a blog post.
1638[Type(SqlSource = "v_comment")]
1639public record Comment(
1640 int Pk,
1641 ID Id,
1642 string Body,
1643 string AuthorName,
1644 ID PostId,
1645 DateTime CreatedAt
1646);
1647
1648/// Categorization tag for posts.
1649[Type(SqlSource = "v_tag")]
1650public record Tag(
1651 int Pk,
1652 ID Id,
1653 string Identifier,
1654 string Name
1655);
1656
1657[Query(ReturnType = typeof(Post), ReturnArray = true, SqlSource = "v_post")]
1658public static partial class Posts;
1659
1660[Query(ReturnType = typeof(Post), SqlSource = "v_post", Arg(Name = "id", Type = "ID", Required = true))]
1661public static partial class PostById;
1662
1663[Query(ReturnType = typeof(Author), ReturnArray = true, SqlSource = "v_author")]
1664public static partial class Authors;
1665
1666[Query(ReturnType = typeof(Author), SqlSource = "v_author", Arg(Name = "id", Type = "ID", Required = true))]
1667public static partial class AuthorById;
1668
1669[Query(ReturnType = typeof(Tag), ReturnArray = true, SqlSource = "v_tag")]
1670public static partial class Tags;
1671"#,
1672 name = config.project_name,
1673 );
1674
1675 fs::write(dir.join("schema.cs"), content).context("Failed to create schema.cs")?;
1676 info!("Created schema/schema.cs");
1677 Ok(())
1678}
1679
1680fn create_swift_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1681 let dir = project_dir.join("schema");
1682 fs::create_dir_all(&dir).context("Failed to create schema/ directory")?;
1683
1684 let content = format!(
1685 r#"// FraiseQL blog schema definition for {name}.
1686
1687import FraiseQL
1688
1689/// Blog author with trinity pattern.
1690@Type(sqlSource: "v_author")
1691struct Author {{
1692 let pk: Int
1693 let id: ID
1694 let identifier: String
1695 let name: String
1696 let email: String
1697 let bio: String?
1698 let createdAt: DateTime
1699 let updatedAt: DateTime
1700}}
1701
1702/// Blog post with trinity pattern.
1703@Type(sqlSource: "v_post")
1704struct Post {{
1705 let pk: Int
1706 let id: ID
1707 let identifier: String
1708 let title: String
1709 let body: String
1710 let published: Bool
1711 let authorId: ID
1712 let createdAt: DateTime
1713 let updatedAt: DateTime
1714}}
1715
1716/// Comment on a blog post.
1717@Type(sqlSource: "v_comment")
1718struct Comment {{
1719 let pk: Int
1720 let id: ID
1721 let body: String
1722 let authorName: String
1723 let postId: ID
1724 let createdAt: DateTime
1725}}
1726
1727/// Categorization tag for posts.
1728@Type(sqlSource: "v_tag")
1729struct Tag {{
1730 let pk: Int
1731 let id: ID
1732 let identifier: String
1733 let name: String
1734}}
1735
1736@Query(returnType: Post.self, returnArray: true, sqlSource: "v_post")
1737func posts() -> [Post] {{ fatalError("Schema definition only") }}
1738
1739@Query(returnType: Post.self, sqlSource: "v_post")
1740func post(id: ID) -> Post {{ fatalError("Schema definition only") }}
1741
1742@Query(returnType: Author.self, returnArray: true, sqlSource: "v_author")
1743func authors() -> [Author] {{ fatalError("Schema definition only") }}
1744
1745@Query(returnType: Author.self, sqlSource: "v_author")
1746func author(id: ID) -> Author {{ fatalError("Schema definition only") }}
1747
1748@Query(returnType: Tag.self, returnArray: true, sqlSource: "v_tag")
1749func tags() -> [Tag] {{ fatalError("Schema definition only") }}
1750"#,
1751 name = config.project_name,
1752 );
1753
1754 fs::write(dir.join("schema.swift"), content).context("Failed to create schema.swift")?;
1755 info!("Created schema/schema.swift");
1756 Ok(())
1757}
1758
1759fn create_scala_skeleton(project_dir: &Path, config: &InitConfig) -> Result<()> {
1760 let dir = project_dir.join("schema");
1761 fs::create_dir_all(&dir).context("Failed to create schema/ directory")?;
1762
1763 let content = format!(
1764 r#"// FraiseQL blog schema definition for {name}.
1765
1766package schema
1767
1768import fraiseql._
1769
1770/// Blog author with trinity pattern.
1771@Type(sqlSource = "v_author")
1772case class Author(
1773 pk: Int,
1774 id: ID,
1775 identifier: String,
1776 name: String,
1777 email: String,
1778 bio: Option[String],
1779 createdAt: DateTime,
1780 updatedAt: DateTime
1781)
1782
1783/// Blog post with trinity pattern.
1784@Type(sqlSource = "v_post")
1785case class Post(
1786 pk: Int,
1787 id: ID,
1788 identifier: String,
1789 title: String,
1790 body: String,
1791 published: Boolean,
1792 authorId: ID,
1793 createdAt: DateTime,
1794 updatedAt: DateTime
1795)
1796
1797/// Comment on a blog post.
1798@Type(sqlSource = "v_comment")
1799case class Comment(
1800 pk: Int,
1801 id: ID,
1802 body: String,
1803 authorName: String,
1804 postId: ID,
1805 createdAt: DateTime
1806)
1807
1808/// Categorization tag for posts.
1809@Type(sqlSource = "v_tag")
1810case class Tag(
1811 pk: Int,
1812 id: ID,
1813 identifier: String,
1814 name: String
1815)
1816
1817@Query(returnType = classOf[Post], returnArray = true, sqlSource = "v_post")
1818def posts(): List[Post] = ???
1819
1820@Query(returnType = classOf[Post], sqlSource = "v_post")
1821def post(id: ID): Post = ???
1822
1823@Query(returnType = classOf[Author], returnArray = true, sqlSource = "v_author")
1824def authors(): List[Author] = ???
1825
1826@Query(returnType = classOf[Author], sqlSource = "v_author")
1827def author(id: ID): Author = ???
1828
1829@Query(returnType = classOf[Tag], returnArray = true, sqlSource = "v_tag")
1830def tags(): List[Tag] = ???
1831"#,
1832 name = config.project_name,
1833 );
1834
1835 fs::write(dir.join("schema.scala"), content).context("Failed to create schema.scala")?;
1836 info!("Created schema/schema.scala");
1837 Ok(())
1838}
1839
1840fn init_git(project_dir: &Path) -> Result<()> {
1841 let status = Command::new("git")
1842 .args(["init"])
1843 .current_dir(project_dir)
1844 .stdout(std::process::Stdio::null())
1845 .stderr(std::process::Stdio::null())
1846 .status();
1847
1848 match status {
1849 Ok(s) if s.success() => {
1850 info!("Initialized git repository");
1851 Ok(())
1852 },
1853 Ok(_) => {
1854 eprintln!("Warning: git init failed. You can initialize git manually.");
1856 Ok(())
1857 },
1858 Err(_) => {
1859 eprintln!("Warning: git not found. Skipping repository initialization.");
1860 Ok(())
1861 },
1862 }
1863}
1864
1865#[cfg(test)]
1866mod tests {
1867 use super::*;
1868
1869 #[test]
1870 fn test_language_from_str() {
1871 assert_eq!(Language::from_str("python").unwrap(), Language::Python);
1872 assert_eq!(Language::from_str("py").unwrap(), Language::Python);
1873 assert_eq!(Language::from_str("typescript").unwrap(), Language::TypeScript);
1874 assert_eq!(Language::from_str("ts").unwrap(), Language::TypeScript);
1875 assert_eq!(Language::from_str("rust").unwrap(), Language::Rust);
1876 assert_eq!(Language::from_str("rs").unwrap(), Language::Rust);
1877 assert_eq!(Language::from_str("java").unwrap(), Language::Java);
1878 assert_eq!(Language::from_str("jav").unwrap(), Language::Java);
1879 assert_eq!(Language::from_str("kotlin").unwrap(), Language::Kotlin);
1880 assert_eq!(Language::from_str("kt").unwrap(), Language::Kotlin);
1881 assert_eq!(Language::from_str("go").unwrap(), Language::Go);
1882 assert_eq!(Language::from_str("golang").unwrap(), Language::Go);
1883 assert_eq!(Language::from_str("csharp").unwrap(), Language::CSharp);
1884 assert_eq!(Language::from_str("c#").unwrap(), Language::CSharp);
1885 assert_eq!(Language::from_str("cs").unwrap(), Language::CSharp);
1886 assert_eq!(Language::from_str("swift").unwrap(), Language::Swift);
1887 assert_eq!(Language::from_str("scala").unwrap(), Language::Scala);
1888 assert_eq!(Language::from_str("sc").unwrap(), Language::Scala);
1889 assert!(Language::from_str("haskell").is_err());
1890 }
1891
1892 #[test]
1893 fn test_language_from_extension() {
1894 assert_eq!(Language::from_extension("py"), Some(Language::Python));
1895 assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
1896 assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
1897 assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
1898 assert_eq!(Language::from_extension("java"), Some(Language::Java));
1899 assert_eq!(Language::from_extension("kt"), Some(Language::Kotlin));
1900 assert_eq!(Language::from_extension("kts"), Some(Language::Kotlin));
1901 assert_eq!(Language::from_extension("go"), Some(Language::Go));
1902 assert_eq!(Language::from_extension("cs"), Some(Language::CSharp));
1903 assert_eq!(Language::from_extension("swift"), Some(Language::Swift));
1904 assert_eq!(Language::from_extension("scala"), Some(Language::Scala));
1905 assert_eq!(Language::from_extension("sc"), Some(Language::Scala));
1906 assert_eq!(Language::from_extension("rb"), None);
1907 assert_eq!(Language::from_extension(""), None);
1908 }
1909
1910 #[test]
1911 fn test_database_from_str() {
1912 assert_eq!(Database::from_str("postgres").unwrap(), Database::Postgres);
1913 assert_eq!(Database::from_str("postgresql").unwrap(), Database::Postgres);
1914 assert_eq!(Database::from_str("pg").unwrap(), Database::Postgres);
1915 assert_eq!(Database::from_str("mysql").unwrap(), Database::Mysql);
1916 assert_eq!(Database::from_str("sqlite").unwrap(), Database::Sqlite);
1917 assert_eq!(Database::from_str("sqlserver").unwrap(), Database::SqlServer);
1918 assert_eq!(Database::from_str("mssql").unwrap(), Database::SqlServer);
1919 assert!(Database::from_str("oracle").is_err());
1920 }
1921
1922 #[test]
1923 fn test_size_from_str() {
1924 assert_eq!(ProjectSize::from_str("xs").unwrap(), ProjectSize::Xs);
1925 assert_eq!(ProjectSize::from_str("s").unwrap(), ProjectSize::S);
1926 assert_eq!(ProjectSize::from_str("m").unwrap(), ProjectSize::M);
1927 assert!(ProjectSize::from_str("l").is_err());
1928 }
1929
1930 #[test]
1931 fn test_database_default_url() {
1932 assert_eq!(Database::Postgres.default_url("myapp"), "postgresql://localhost/myapp");
1933 assert_eq!(Database::Sqlite.default_url("myapp"), "myapp.db");
1934 }
1935
1936 #[test]
1937 fn test_init_creates_project() {
1938 let tmp = tempfile::tempdir().unwrap();
1939 let project_dir = tmp.path().join("test_project");
1940
1941 let config = InitConfig {
1942 project_name: project_dir.to_string_lossy().to_string(),
1943 language: Language::Python,
1944 database: Database::Postgres,
1945 size: ProjectSize::S,
1946 no_git: true,
1947 };
1948
1949 run(&config).unwrap();
1950
1951 assert!(project_dir.join(".gitignore").exists());
1953 assert!(project_dir.join("fraiseql.toml").exists());
1954 assert!(project_dir.join("schema.json").exists());
1955 assert!(project_dir.join("db/0_schema/01_write/011_tb_author.sql").exists());
1956 assert!(project_dir.join("db/0_schema/01_write/012_tb_post.sql").exists());
1957 assert!(project_dir.join("db/0_schema/01_write/013_tb_comment.sql").exists());
1958 assert!(project_dir.join("db/0_schema/01_write/014_tb_tag.sql").exists());
1959 assert!(project_dir.join("db/0_schema/02_read/021_v_author.sql").exists());
1960 assert!(project_dir.join("db/0_schema/03_functions/031_fn_author_crud.sql").exists());
1961 assert!(project_dir.join("schema/schema.py").exists());
1963 assert!(!project_dir.join("schema/schema.ts").exists());
1964 assert!(!project_dir.join("schema/schema.rs").exists());
1965 }
1966
1967 #[test]
1968 fn test_init_xs_layout() {
1969 let tmp = tempfile::tempdir().unwrap();
1970 let project_dir = tmp.path().join("test_xs");
1971
1972 let config = InitConfig {
1973 project_name: project_dir.to_string_lossy().to_string(),
1974 language: Language::TypeScript,
1975 database: Database::Postgres,
1976 size: ProjectSize::Xs,
1977 no_git: true,
1978 };
1979
1980 run(&config).unwrap();
1981
1982 assert!(project_dir.join("db/0_schema/schema.sql").exists());
1983 assert!(project_dir.join("schema/schema.ts").exists());
1984
1985 assert!(!project_dir.join("db/0_schema/01_write").exists());
1987 }
1988
1989 #[test]
1990 fn test_init_m_layout() {
1991 let tmp = tempfile::tempdir().unwrap();
1992 let project_dir = tmp.path().join("test_m");
1993
1994 let config = InitConfig {
1995 project_name: project_dir.to_string_lossy().to_string(),
1996 language: Language::Rust,
1997 database: Database::Postgres,
1998 size: ProjectSize::M,
1999 no_git: true,
2000 };
2001
2002 run(&config).unwrap();
2003
2004 assert!(project_dir.join("db/0_schema/01_write/author/tb_author.sql").exists());
2005 assert!(project_dir.join("db/0_schema/01_write/post/tb_post.sql").exists());
2006 assert!(project_dir.join("db/0_schema/02_read/author/v_author.sql").exists());
2007 assert!(project_dir.join("db/0_schema/03_functions/author/fn_author_crud.sql").exists());
2008 assert!(project_dir.join("schema/schema.rs").exists());
2009 }
2010
2011 #[test]
2012 fn test_init_refuses_existing_dir() {
2013 let tmp = tempfile::tempdir().unwrap();
2014 let project_dir = tmp.path().join("existing");
2015
2016 fs::create_dir(&project_dir).unwrap();
2017
2018 let config = InitConfig {
2019 project_name: project_dir.to_string_lossy().to_string(),
2020 language: Language::Python,
2021 database: Database::Postgres,
2022 size: ProjectSize::S,
2023 no_git: true,
2024 };
2025
2026 let result = run(&config);
2027 assert!(result.is_err());
2028 assert!(result.unwrap_err().to_string().contains("already exists"));
2029 }
2030
2031 #[test]
2032 fn test_toml_config_is_valid() {
2033 let tmp = tempfile::tempdir().unwrap();
2034 let project_dir = tmp.path().join("toml_test");
2035
2036 let config = InitConfig {
2037 project_name: project_dir.to_string_lossy().to_string(),
2038 language: Language::Python,
2039 database: Database::Postgres,
2040 size: ProjectSize::S,
2041 no_git: true,
2042 };
2043
2044 run(&config).unwrap();
2045
2046 let toml_content = fs::read_to_string(project_dir.join("fraiseql.toml")).unwrap();
2048 let parsed: toml::Value = toml::from_str(&toml_content).unwrap();
2049 assert!(parsed["project"]["name"].as_str().is_some());
2051 }
2052
2053 #[test]
2054 fn test_schema_json_is_valid() {
2055 let tmp = tempfile::tempdir().unwrap();
2056 let project_dir = tmp.path().join("json_test");
2057
2058 let config = InitConfig {
2059 project_name: project_dir.to_string_lossy().to_string(),
2060 language: Language::Python,
2061 database: Database::Postgres,
2062 size: ProjectSize::Xs,
2063 no_git: true,
2064 };
2065
2066 run(&config).unwrap();
2067
2068 let json_content = fs::read_to_string(project_dir.join("schema.json")).unwrap();
2069 let parsed: serde_json::Value = serde_json::from_str(&json_content).unwrap();
2070
2071 assert!(parsed["types"].is_array(), "types should be an array");
2073 assert!(parsed["queries"].is_array(), "queries should be an array");
2074 assert_eq!(parsed["types"][0]["name"], "Author");
2075 assert_eq!(parsed["types"][1]["name"], "Post");
2076 assert_eq!(parsed["types"][2]["name"], "Comment");
2077 assert_eq!(parsed["types"][3]["name"], "Tag");
2078 assert_eq!(parsed["queries"][0]["name"], "posts");
2079 assert_eq!(parsed["version"], "2.0.0");
2080 }
2081}