From 67ae0ad89735b5d0e4b8a4ad911b2c27de138828 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Fri, 10 Apr 2026 00:08:26 +0300 Subject: [PATCH] feat: add Ent as Go ORM option Add Ent (entgo.io) by Meta as a third Go ORM option alongside GORM and sqlc. Ent provides code-first schema definitions with compile-time safety and a graph traversal API. - Schema: add "ent" to GoOrmSchema enum - Templates: go.mod.hbs (deps), database.go.hbs (database/sql init), models.go.hbs (input types), handlers.go.hbs (CRUD for all 4 frameworks), main.go.hbs (init block), ent/schema/ (User + Post schemas), ent/generate.go - CLI: prompt option, post-install display name - Web: constant.ts entry, tech-resource-links - Processors: readme-generator (features, structure, db setup) Scaffolds compile with go build for all framework combos: gin+ent, echo+ent, fiber+ent, chi+ent, gin+ent+grpc, fiber+ent+none-logging --- .../cli/src/helpers/core/post-installation.ts | 1 + apps/cli/src/prompts/go-ecosystem.ts | 5 + apps/web/src/lib/constant.ts | 8 + apps/web/src/lib/tech-resource-links.ts | 1 + .../src/processors/readme-generator.ts | 29 + .../templates/go-base/cmd/server/main.go.hbs | 29 +- .../templates/go-base/ent/generate.go.hbs | 5 + .../templates/go-base/ent/schema/post.go.hbs | 46 ++ .../templates/go-base/ent/schema/user.go.hbs | 40 ++ .../templates/go-base/go.mod.hbs | 5 + .../go-base/internal/database/database.go.hbs | 86 +++ .../go-base/internal/handlers/handlers.go.hbs | 501 +++++++++++++++++- .../go-base/internal/models/models.go.hbs | 29 + packages/types/src/option-metadata.ts | 1 + packages/types/src/schemas.ts | 2 +- 15 files changed, 779 insertions(+), 9 deletions(-) create mode 100644 packages/template-generator/templates/go-base/ent/generate.go.hbs create mode 100644 packages/template-generator/templates/go-base/ent/schema/post.go.hbs create mode 100644 packages/template-generator/templates/go-base/ent/schema/user.go.hbs diff --git a/apps/cli/src/helpers/core/post-installation.ts b/apps/cli/src/helpers/core/post-installation.ts index 671ef6dbd..64466d9a9 100644 --- a/apps/cli/src/helpers/core/post-installation.ts +++ b/apps/cli/src/helpers/core/post-installation.ts @@ -789,6 +789,7 @@ function displayGoInstructions(config: ProjectConfig & { depsInstalled: boolean const ormNames: Record = { gorm: "GORM", sqlc: "sqlc", + ent: "Ent", }; output += `${pc.cyan("•")} Database: ${ormNames[goOrm] || goOrm}\n`; } diff --git a/apps/cli/src/prompts/go-ecosystem.ts b/apps/cli/src/prompts/go-ecosystem.ts index 9ff96ff10..07a94a28c 100644 --- a/apps/cli/src/prompts/go-ecosystem.ts +++ b/apps/cli/src/prompts/go-ecosystem.ts @@ -59,6 +59,11 @@ export async function getGoOrmChoice(goOrm?: GoOrm) { label: "sqlc", hint: "Generate type-safe Go code from SQL", }, + { + value: "ent" as const, + label: "Ent", + hint: "Code-first ORM by Meta with graph traversal API, 15k+ stars", + }, { value: "none" as const, label: "None", diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index d8fdf195b..31128da05 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -3047,6 +3047,14 @@ export const TECH_OPTIONS: Record< color: "from-orange-500 to-orange-700", default: false, }, + { + id: "ent", + name: "Ent", + description: "Code-first ORM by Meta with compile-time safety and graph traversal API", + icon: "", + color: "from-blue-500 to-indigo-600", + default: false, + }, { id: "none", name: "No ORM", diff --git a/apps/web/src/lib/tech-resource-links.ts b/apps/web/src/lib/tech-resource-links.ts index e820a80df..d4a8938e4 100644 --- a/apps/web/src/lib/tech-resource-links.ts +++ b/apps/web/src/lib/tech-resource-links.ts @@ -744,6 +744,7 @@ const BASE_LINKS: LinkMap = { chi: { docsUrl: "https://go-chi.io/", githubUrl: "https://github.com/go-chi/chi" }, gorm: { docsUrl: "https://gorm.io/docs/", githubUrl: "https://github.com/go-gorm/gorm" }, sqlc: { docsUrl: "https://docs.sqlc.dev/", githubUrl: "https://github.com/sqlc-dev/sqlc" }, + ent: { docsUrl: "https://entgo.io/docs/getting-started/", githubUrl: "https://github.com/ent/ent" }, "grpc-go": { docsUrl: "https://grpc.io/docs/languages/go/quickstart/", githubUrl: "https://github.com/grpc/grpc-go", diff --git a/packages/template-generator/src/processors/readme-generator.ts b/packages/template-generator/src/processors/readme-generator.ts index d66bd1dc8..2780edad4 100644 --- a/packages/template-generator/src/processors/readme-generator.ts +++ b/packages/template-generator/src/processors/readme-generator.ts @@ -1244,6 +1244,8 @@ function generateGoReadmeContent(config: ProjectConfig): string { features.push("- **GORM** - Full-featured ORM for Go"); } else if (goOrm === "sqlc") { features.push("- **SQLc** - Generate type-safe code from SQL"); + } else if (goOrm === "ent") { + features.push("- **Ent** - Code-first ORM by Meta with graph traversal API"); } // API @@ -1299,6 +1301,11 @@ function generateGoReadmeContent(config: ProjectConfig): string { structure.push("│ │ └── models.go"); structure.push("│ └── handlers/ # HTTP handlers"); structure.push("│ └── handlers.go"); + } else if (goOrm === "ent") { + structure.push("│ ├── models/ # Request/response types"); + structure.push("│ │ └── models.go"); + structure.push("│ └── handlers/ # HTTP handlers"); + structure.push("│ └── handlers.go"); } } else if (auth === "go-better-auth") { structure.push("├── internal/"); @@ -1396,6 +1403,28 @@ cp .env.example .env \`\`\`bash sqlc generate \`\`\` +`; + } else if (goOrm === "ent") { + databaseSetup = ` +## Database Setup + +This project uses Ent (by Meta) as the ORM. To set up: + +1. Copy the environment file: +\`\`\`bash +cp .env.example .env +\`\`\` + +2. Update \`DATABASE_URL\` in \`.env\` with your database connection string. + +3. Generate Ent client code from schemas: +\`\`\`bash +go generate ./ent +\`\`\` + +Supported databases: +- SQLite (default): leave \`DATABASE_URL\` empty +- PostgreSQL: \`DATABASE_URL=postgres://user:pass@localhost:5432/dbname?sslmode=disable\` `; } diff --git a/packages/template-generator/templates/go-base/cmd/server/main.go.hbs b/packages/template-generator/templates/go-base/cmd/server/main.go.hbs index bf033d2be..4da3db6a4 100644 --- a/packages/template-generator/templates/go-base/cmd/server/main.go.hbs +++ b/packages/template-generator/templates/go-base/cmd/server/main.go.hbs @@ -30,7 +30,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" {{/if}} -{{#if (or (eq goOrm "gorm") (eq goOrm "sqlc"))}} +{{#if (or (or (eq goOrm "gorm") (eq goOrm "sqlc")) (eq goOrm "ent"))}} "{{projectName}}/internal/database" {{/if}} @@ -191,6 +191,33 @@ func main() { {{/if}} _ = pool // Use pool in your handlers {{/if}} +{{#if (eq goOrm "ent")}} + // Initialize database (run `go generate ./ent` to use Ent client) + db, err := database.InitDB() + if err != nil { +{{#if (eq goLogging "zap")}} + logger.Fatal("Failed to connect to database", zap.Error(err)) +{{else if (eq goLogging "zerolog")}} + logger.Fatal().Err(err).Msg("Failed to connect to database") +{{else if (eq goLogging "slog")}} + logger.Error("Failed to connect to database", "error", err) + os.Exit(1) +{{else}} + panic("Failed to connect to database: " + err.Error()) +{{/if}} + } + defer database.Close() +{{#if (eq goLogging "zap")}} + logger.Info("Database connected successfully") +{{/if}} +{{#if (eq goLogging "zerolog")}} + logger.Info().Msg("Database connected successfully") +{{/if}} +{{#if (eq goLogging "slog")}} + logger.Info("Database connected successfully") +{{/if}} + _ = db // Use db in your handlers +{{/if}} {{#if (or (or (or (or (or (eq goWebFramework "gin") (eq goWebFramework "echo")) (eq goWebFramework "fiber")) (eq goWebFramework "chi")) (eq goApi "grpc-go")) (eq auth "go-better-auth"))}} // Get host from environment diff --git a/packages/template-generator/templates/go-base/ent/generate.go.hbs b/packages/template-generator/templates/go-base/ent/generate.go.hbs new file mode 100644 index 000000000..892bf2f76 --- /dev/null +++ b/packages/template-generator/templates/go-base/ent/generate.go.hbs @@ -0,0 +1,5 @@ +{{#if (eq goOrm "ent")}} +package ent + +//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema +{{/if}} diff --git a/packages/template-generator/templates/go-base/ent/schema/post.go.hbs b/packages/template-generator/templates/go-base/ent/schema/post.go.hbs new file mode 100644 index 000000000..b4bbd41e6 --- /dev/null +++ b/packages/template-generator/templates/go-base/ent/schema/post.go.hbs @@ -0,0 +1,46 @@ +{{#if (eq goOrm "ent")}} +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/schema/edge" + "entgo.io/ent/schema/field" +) + +// Post holds the schema definition for the Post entity. +type Post struct { + ent.Schema +} + +// Fields of the Post. +func (Post) Fields() []ent.Field { + return []ent.Field{ + field.String("title"). + NotEmpty(), + field.Text("content"). + Default(""), + field.Bool("published"). + Default(false), + field.Int("author_id"), + field.Time("created_at"). + Default(time.Now). + Immutable(), + field.Time("updated_at"). + Default(time.Now). + UpdateDefault(time.Now), + } +} + +// Edges of the Post. +func (Post) Edges() []ent.Edge { + return []ent.Edge{ + edge.From("author", User.Type). + Ref("posts"). + Field("author_id"). + Unique(). + Required(), + } +} +{{/if}} diff --git a/packages/template-generator/templates/go-base/ent/schema/user.go.hbs b/packages/template-generator/templates/go-base/ent/schema/user.go.hbs new file mode 100644 index 000000000..a00805c7f --- /dev/null +++ b/packages/template-generator/templates/go-base/ent/schema/user.go.hbs @@ -0,0 +1,40 @@ +{{#if (eq goOrm "ent")}} +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/schema/edge" + "entgo.io/ent/schema/field" +) + +// User holds the schema definition for the User entity. +type User struct { + ent.Schema +} + +// Fields of the User. +func (User) Fields() []ent.Field { + return []ent.Field{ + field.String("name"). + NotEmpty(), + field.String("email"). + NotEmpty(). + Unique(), + field.Time("created_at"). + Default(time.Now). + Immutable(), + field.Time("updated_at"). + Default(time.Now). + UpdateDefault(time.Now), + } +} + +// Edges of the User. +func (User) Edges() []ent.Edge { + return []ent.Edge{ + edge.To("posts", Post.Type), + } +} +{{/if}} diff --git a/packages/template-generator/templates/go-base/go.mod.hbs b/packages/template-generator/templates/go-base/go.mod.hbs index 6f4a4d516..181572600 100644 --- a/packages/template-generator/templates/go-base/go.mod.hbs +++ b/packages/template-generator/templates/go-base/go.mod.hbs @@ -29,6 +29,11 @@ require ( {{#if (eq goOrm "sqlc")}} github.com/jackc/pgx/v5 v5.9.1 {{/if}} +{{#if (eq goOrm "ent")}} + entgo.io/ent v0.14.0 + github.com/lib/pq v1.10.9 + github.com/mattn/go-sqlite3 v1.14.24 +{{/if}} {{#if (eq goApi "grpc-go")}} google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 diff --git a/packages/template-generator/templates/go-base/internal/database/database.go.hbs b/packages/template-generator/templates/go-base/internal/database/database.go.hbs index ca11defee..3073aeae3 100644 --- a/packages/template-generator/templates/go-base/internal/database/database.go.hbs +++ b/packages/template-generator/templates/go-base/internal/database/database.go.hbs @@ -92,3 +92,89 @@ func Close() { } } {{/if}} +{{#if (eq goOrm "ent")}} +package database + +import ( + "database/sql" + "os" + "strings" + + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" +) + +var DB *sql.DB + +// InitDB initializes the database connection. +// After running `go generate ./ent`, you can switch to using the Ent client directly. +func InitDB() (*sql.DB, error) { + dsn := os.Getenv("DATABASE_URL") + + var db *sql.DB + var err error + + if dsn == "" { + // Default to SQLite for development + db, err = sql.Open("sqlite3", "file:app.db?cache=shared&_fk=1") + } else if strings.HasPrefix(dsn, "postgres") { + db, err = sql.Open("postgres", dsn) + } else { + // Assume SQLite file path + db, err = sql.Open("sqlite3", "file:"+dsn+"?cache=shared&_fk=1") + } + + if err != nil { + return nil, err + } + + // Verify connection + if err := db.Ping(); err != nil { + db.Close() + return nil, err + } + + // Create tables if they don't exist (SQLite dev mode) + if dsn == "" || !strings.HasPrefix(dsn, "postgres") { + if _, err := db.Exec(sqliteSchema); err != nil { + db.Close() + return nil, err + } + } + + DB = db + return DB, nil +} + +// GetDB returns the database instance +func GetDB() *sql.DB { + return DB +} + +// Close closes the database connection +func Close() { + if DB != nil { + DB.Close() + } +} + +const sqliteSchema = ` +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + published BOOLEAN NOT NULL DEFAULT 0, + author_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +` +{{/if}} diff --git a/packages/template-generator/templates/go-base/internal/handlers/handlers.go.hbs b/packages/template-generator/templates/go-base/internal/handlers/handlers.go.hbs index 7c2d23ec4..6aab960d1 100644 --- a/packages/template-generator/templates/go-base/internal/handlers/handlers.go.hbs +++ b/packages/template-generator/templates/go-base/internal/handlers/handlers.go.hbs @@ -4,12 +4,12 @@ package handlers {{#if (eq goWebFramework "gin")}} import ( "net/http" -{{#if (or (eq goOrm "gorm") (eq goOrm "sqlc"))}} +{{#if (or (or (eq goOrm "gorm") (eq goOrm "sqlc")) (eq goOrm "ent"))}} "strconv" {{/if}} "github.com/gin-gonic/gin" -{{#if (or (eq goOrm "gorm") (eq goOrm "sqlc"))}} +{{#if (or (or (eq goOrm "gorm") (eq goOrm "sqlc")) (eq goOrm "ent"))}} "{{projectName}}/internal/database" "{{projectName}}/internal/models" @@ -459,6 +459,153 @@ func DeletePost(c *gin.Context) { c.JSON(http.StatusNoContent, nil) } +{{else if (eq goOrm "ent")}} +// GetUsers returns all users +func GetUsers(c *gin.Context) { + rows, err := database.GetDB().Query("SELECT id, name, email, created_at, updated_at FROM users ORDER BY id") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var users []models.UserCreate + for rows.Next() { + var id int + var name, email string + var createdAt, updatedAt string + if err := rows.Scan(&id, &name, &email, &createdAt, &updatedAt); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + users = append(users, models.UserCreate{Name: name, Email: email}) + } + + c.JSON(http.StatusOK, users) +} + +// GetUser returns a user by ID +func GetUser(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + var name, email string + err = database.GetDB().QueryRow("SELECT name, email FROM users WHERE id = ?", id).Scan(&name, &email) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{"id": id, "name": name, "email": email}) +} + +// CreateUser creates a new user +func CreateUser(c *gin.Context) { + var input models.UserCreate + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + result, err := database.GetDB().Exec("INSERT INTO users (name, email) VALUES (?, ?)", input.Name, input.Email) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + id, _ := result.LastInsertId() + c.JSON(http.StatusCreated, gin.H{"id": id, "name": input.Name, "email": input.Email}) +} + +// UpdateUser updates a user +func UpdateUser(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + var input models.UserUpdate + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if input.Name != nil { + database.GetDB().Exec("UPDATE users SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", *input.Name, id) + } + if input.Email != nil { + database.GetDB().Exec("UPDATE users SET email = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", *input.Email, id) + } + + c.JSON(http.StatusOK, gin.H{"id": id, "message": "User updated"}) +} + +// DeleteUser deletes a user +func DeleteUser(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + _, err = database.GetDB().Exec("DELETE FROM users WHERE id = ?", id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusNoContent, nil) +} + +// GetPosts returns all posts +func GetPosts(c *gin.Context) { + rows, err := database.GetDB().Query("SELECT id, title, content, published, author_id, created_at, updated_at FROM posts ORDER BY id") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var posts []gin.H + for rows.Next() { + var id, authorID int + var title, content string + var published bool + var createdAt, updatedAt string + if err := rows.Scan(&id, &title, &content, &published, &authorID, &createdAt, &updatedAt); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + posts = append(posts, gin.H{"id": id, "title": title, "content": content, "published": published, "author_id": authorID}) + } + + if posts == nil { + posts = []gin.H{} + } + c.JSON(http.StatusOK, posts) +} + +// CreatePost creates a new post +func CreatePost(c *gin.Context) { + var input models.PostCreate + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + result, err := database.GetDB().Exec("INSERT INTO posts (title, content, author_id) VALUES (?, ?, ?)", input.Title, input.Content, input.AuthorID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + id, _ := result.LastInsertId() + c.JSON(http.StatusCreated, gin.H{"id": id, "title": input.Title, "content": input.Content, "author_id": input.AuthorID}) +} {{else}} // HealthCheck returns server health status func HealthCheck(c *gin.Context) { @@ -472,12 +619,12 @@ func HealthCheck(c *gin.Context) { {{#if (eq goWebFramework "echo")}} import ( "net/http" -{{#if (or (eq goOrm "gorm") (eq goOrm "sqlc"))}} +{{#if (or (or (eq goOrm "gorm") (eq goOrm "sqlc")) (eq goOrm "ent"))}} "strconv" {{/if}} "github.com/labstack/echo/v4" -{{#if (or (eq goOrm "gorm") (eq goOrm "sqlc"))}} +{{#if (or (or (eq goOrm "gorm") (eq goOrm "sqlc")) (eq goOrm "ent"))}} "{{projectName}}/internal/database" "{{projectName}}/internal/models" @@ -881,6 +1028,118 @@ func DeletePost(c echo.Context) error { return c.NoContent(http.StatusNoContent) } +{{else if (eq goOrm "ent")}} +// GetUsers returns all users +func GetUsers(c echo.Context) error { + rows, err := database.GetDB().Query("SELECT id, name, email FROM users ORDER BY id") + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + defer rows.Close() + + var users []map[string]any + for rows.Next() { + var id int + var name, email string + if err := rows.Scan(&id, &name, &email); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + users = append(users, map[string]any{"id": id, "name": name, "email": email}) + } + + if users == nil { + users = []map[string]any{} + } + return c.JSON(http.StatusOK, users) +} + +// GetUser returns a user by ID +func GetUser(c echo.Context) error { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid ID"}) + } + + var name, email string + err = database.GetDB().QueryRow("SELECT name, email FROM users WHERE id = ?", id).Scan(&name, &email) + if err != nil { + return c.JSON(http.StatusNotFound, map[string]string{"error": "User not found"}) + } + + return c.JSON(http.StatusOK, map[string]any{"id": id, "name": name, "email": email}) +} + +// CreateUser creates a new user +func CreateUser(c echo.Context) error { + var input models.UserCreate + if err := c.Bind(&input); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + result, err := database.GetDB().Exec("INSERT INTO users (name, email) VALUES (?, ?)", input.Name, input.Email) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + id, _ := result.LastInsertId() + return c.JSON(http.StatusCreated, map[string]any{"id": id, "name": input.Name, "email": input.Email}) +} + +// DeleteUser deletes a user +func DeleteUser(c echo.Context) error { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid ID"}) + } + + _, err = database.GetDB().Exec("DELETE FROM users WHERE id = ?", id) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.NoContent(http.StatusNoContent) +} + +// GetPosts returns all posts +func GetPosts(c echo.Context) error { + rows, err := database.GetDB().Query("SELECT id, title, content, published, author_id FROM posts ORDER BY id") + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + defer rows.Close() + + var posts []map[string]any + for rows.Next() { + var id, authorID int + var title, content string + var published bool + if err := rows.Scan(&id, &title, &content, &published, &authorID); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + posts = append(posts, map[string]any{"id": id, "title": title, "content": content, "published": published, "author_id": authorID}) + } + + if posts == nil { + posts = []map[string]any{} + } + return c.JSON(http.StatusOK, posts) +} + +// CreatePost creates a new post +func CreatePost(c echo.Context) error { + var input models.PostCreate + if err := c.Bind(&input); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + result, err := database.GetDB().Exec("INSERT INTO posts (title, content, author_id) VALUES (?, ?, ?)", input.Title, input.Content, input.AuthorID) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + id, _ := result.LastInsertId() + return c.JSON(http.StatusCreated, map[string]any{"id": id, "title": input.Title, "content": input.Content, "author_id": input.AuthorID}) +} {{else}} // HealthCheck returns server health status func HealthCheck(c echo.Context) error { @@ -893,13 +1152,13 @@ func HealthCheck(c echo.Context) error { {{/if}} {{#if (eq goWebFramework "fiber")}} import ( -{{#if (or (eq goOrm "gorm") (eq goOrm "sqlc"))}} +{{#if (or (or (eq goOrm "gorm") (eq goOrm "sqlc")) (eq goOrm "ent"))}} "net/http" "strconv" {{/if}} "github.com/gofiber/fiber/v2" -{{#if (or (eq goOrm "gorm") (eq goOrm "sqlc"))}} +{{#if (or (or (eq goOrm "gorm") (eq goOrm "sqlc")) (eq goOrm "ent"))}} "{{projectName}}/internal/database" "{{projectName}}/internal/models" @@ -1292,6 +1551,102 @@ func DeletePost(c *fiber.Ctx) error { return c.SendStatus(http.StatusNoContent) } +{{else if (eq goOrm "ent")}} +// GetUsers returns all users +func GetUsers(c *fiber.Ctx) error { + rows, err := database.GetDB().Query("SELECT id, name, email FROM users ORDER BY id") + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + defer rows.Close() + + var users []fiber.Map + for rows.Next() { + var id int + var name, email string + if err := rows.Scan(&id, &name, &email); err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + users = append(users, fiber.Map{"id": id, "name": name, "email": email}) + } + + if users == nil { + users = []fiber.Map{} + } + return c.JSON(users) +} + +// CreateUser creates a new user +func CreateUser(c *fiber.Ctx) error { + var input models.UserCreate + if err := c.BodyParser(&input); err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + + result, err := database.GetDB().Exec("INSERT INTO users (name, email) VALUES (?, ?)", input.Name, input.Email) + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + id, _ := result.LastInsertId() + return c.Status(http.StatusCreated).JSON(fiber.Map{"id": id, "name": input.Name, "email": input.Email}) +} + +// DeleteUser deletes a user +func DeleteUser(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Invalid ID"}) + } + + _, err = database.GetDB().Exec("DELETE FROM users WHERE id = ?", id) + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + return c.SendStatus(http.StatusNoContent) +} + +// GetPosts returns all posts +func GetPosts(c *fiber.Ctx) error { + rows, err := database.GetDB().Query("SELECT id, title, content, published, author_id FROM posts ORDER BY id") + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + defer rows.Close() + + var posts []fiber.Map + for rows.Next() { + var id, authorID int + var title, content string + var published bool + if err := rows.Scan(&id, &title, &content, &published, &authorID); err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + posts = append(posts, fiber.Map{"id": id, "title": title, "content": content, "published": published, "author_id": authorID}) + } + + if posts == nil { + posts = []fiber.Map{} + } + return c.JSON(posts) +} + +// CreatePost creates a new post +func CreatePost(c *fiber.Ctx) error { + var input models.PostCreate + if err := c.BodyParser(&input); err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + } + + result, err := database.GetDB().Exec("INSERT INTO posts (title, content, author_id) VALUES (?, ?, ?)", input.Title, input.Content, input.AuthorID) + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + + id, _ := result.LastInsertId() + return c.Status(http.StatusCreated).JSON(fiber.Map{"id": id, "title": input.Title, "content": input.Content, "author_id": input.AuthorID}) +} {{else}} // HealthCheck returns server health status func HealthCheck(c *fiber.Ctx) error { @@ -1306,7 +1661,7 @@ func HealthCheck(c *fiber.Ctx) error { import ( "encoding/json" "net/http" -{{#if (or (eq goOrm "gorm") (eq goOrm "sqlc"))}} +{{#if (or (or (eq goOrm "gorm") (eq goOrm "sqlc")) (eq goOrm "ent"))}} "strconv" "github.com/go-chi/chi/v5" @@ -1860,6 +2215,138 @@ func DeletePost(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +{{else if (eq goOrm "ent")}} +// GetUsers returns all users +func GetUsers(w http.ResponseWriter, r *http.Request) { + rows, err := database.GetDB().Query("SELECT id, name, email FROM users ORDER BY id") + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + defer rows.Close() + + var users []map[string]any + for rows.Next() { + var id int + var name, email string + if err := rows.Scan(&id, &name, &email); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + users = append(users, map[string]any{"id": id, "name": name, "email": email}) + } + + if users == nil { + users = []map[string]any{} + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(users) +} + +// CreateUser creates a new user +func CreateUser(w http.ResponseWriter, r *http.Request) { + var input models.UserCreate + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + result, err := database.GetDB().Exec("INSERT INTO users (name, email) VALUES (?, ?)", input.Name, input.Email) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + id, _ := result.LastInsertId() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]any{"id": id, "name": input.Name, "email": input.Email}) +} + +// DeleteUser deletes a user +func DeleteUser(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(chi.URLParam(r, "id")) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Invalid ID"}) + return + } + + _, err = database.GetDB().Exec("DELETE FROM users WHERE id = ?", id) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// GetPosts returns all posts +func GetPosts(w http.ResponseWriter, r *http.Request) { + rows, err := database.GetDB().Query("SELECT id, title, content, published, author_id FROM posts ORDER BY id") + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + defer rows.Close() + + var posts []map[string]any + for rows.Next() { + var id, authorID int + var title, content string + var published bool + if err := rows.Scan(&id, &title, &content, &published, &authorID); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + posts = append(posts, map[string]any{"id": id, "title": title, "content": content, "published": published, "author_id": authorID}) + } + + if posts == nil { + posts = []map[string]any{} + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(posts) +} + +// CreatePost creates a new post +func CreatePost(w http.ResponseWriter, r *http.Request) { + var input models.PostCreate + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + result, err := database.GetDB().Exec("INSERT INTO posts (title, content, author_id) VALUES (?, ?, ?)", input.Title, input.Content, input.AuthorID) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + id, _ := result.LastInsertId() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]any{"id": id, "title": input.Title, "content": input.Content, "author_id": input.AuthorID}) +} {{else}} // HealthCheck returns server health status func HealthCheck(w http.ResponseWriter, r *http.Request) { diff --git a/packages/template-generator/templates/go-base/internal/models/models.go.hbs b/packages/template-generator/templates/go-base/internal/models/models.go.hbs index c09c98fc7..070121c97 100644 --- a/packages/template-generator/templates/go-base/internal/models/models.go.hbs +++ b/packages/template-generator/templates/go-base/internal/models/models.go.hbs @@ -110,3 +110,32 @@ type PostUpdate struct { Published *bool `json:"published,omitempty"` } {{/if}} +{{#if (eq goOrm "ent")}} +package models + +// UserCreate is used for creating a new user +type UserCreate struct { + Name string `json:"name"` + Email string `json:"email"` +} + +// UserUpdate is used for updating a user +type UserUpdate struct { + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` +} + +// PostCreate is used for creating a new post +type PostCreate struct { + Title string `json:"title"` + Content string `json:"content"` + AuthorID int `json:"author_id"` +} + +// PostUpdate is used for updating a post +type PostUpdate struct { + Title *string `json:"title,omitempty"` + Content *string `json:"content,omitempty"` + Published *bool `json:"published,omitempty"` +} +{{/if}} diff --git a/packages/types/src/option-metadata.ts b/packages/types/src/option-metadata.ts index c915bae82..dd9aab829 100644 --- a/packages/types/src/option-metadata.ts +++ b/packages/types/src/option-metadata.ts @@ -599,6 +599,7 @@ const EXACT_LABEL_OVERRIDES: Partial