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