Generate ent schema in admin code. (#127)

This commit is contained in:
Mike Stefanello 2025-08-04 08:32:10 -04:00 committed by GitHub
parent 67a97832a5
commit 9e6d9fd063
13 changed files with 303 additions and 142 deletions

View file

@ -216,7 +216,6 @@ The container is located at `pkg/services/container.go` and is meant to house al
- Configuration - Configuration
- Database - Database
- Files - Files
- Graph
- Mail - Mail
- ORM - ORM
- Tasks - Tasks
@ -433,7 +432,7 @@ Users with admin [access](#access) will see additional links on the default side
In order to automatically and dynamically provide admin functionality for entities, code generation is used by means of leveraging Ent's [extension API](https://entgo.io/docs/extensions) which makes generating code using the Ent graph schema very easy. A [custom extension](https://github.com/mikestefanello/pagoda/blob/master/ent/admin/extension.go) is provided to generate code that provides flat entity type structs and handler code that work directly with Echo. So, both of those are required in order for any of this to work. Whenever you modify one of your entity types or generate a new one, the admin code will also automatically generate. In order to automatically and dynamically provide admin functionality for entities, code generation is used by means of leveraging Ent's [extension API](https://entgo.io/docs/extensions) which makes generating code using the Ent graph schema very easy. A [custom extension](https://github.com/mikestefanello/pagoda/blob/master/ent/admin/extension.go) is provided to generate code that provides flat entity type structs and handler code that work directly with Echo. So, both of those are required in order for any of this to work. Whenever you modify one of your entity types or generate a new one, the admin code will also automatically generate.
Without going in to too much detail here, the generated code provides a [handler](https://github.com/mikestefanello/pagoda/blob/master/ent/admin/handler.go) that is then used by a provided [web handler](https://github.com/mikestefanello/pagoda/blob/master/pkg/handlers/admin.go) to power all the routes used in the admin UI. While the rest of the related code should be simple enough to follow, it's worth calling attention to the highly-dynamic [entity form](https://github.com/mikestefanello/pagoda/blob/master/pkg/ui/forms/admin_entity.go) that is constructed using the _Ent_ graph data structure. Without going in to too much detail here, the generated code provides a [handler](https://github.com/mikestefanello/pagoda/blob/master/ent/admin/handler.go) that is then used by a provided [web handler](https://github.com/mikestefanello/pagoda/blob/master/pkg/handlers/admin.go) to power all the routes used in the admin UI. While the rest of the related code should be simple enough to follow, it's worth calling attention to the highly dynamic [entity form](https://github.com/mikestefanello/pagoda/blob/master/pkg/ui/forms/admin_entity.go) that is constructed using the _Ent_ graph data structure.
### Access ### Access
@ -451,7 +450,7 @@ Since the generated code is completely dynamic, all entity functionality related
* Determine which tests should be included and provide them. * Determine which tests should be included and provide them.
* Inline validation. * Inline validation.
* Either exposed sorting, or allow the _handler_ to be configured with sort criteria for each type. * Either exposed sorting or allow the _handler_ to be configured with sort criteria for each type.
* Exposed filters. * Exposed filters.
* Support all field types (types such as _JSON_ as currently not supported). * Support all field types (types such as _JSON_ as currently not supported).
* Control which fields appear in the entity list table. * Control which fields appear in the entity list table.

View file

@ -30,55 +30,55 @@ func NewHandler(client *ent.Client, cfg HandlerConfig) *Handler {
} }
} }
func (h *Handler) Create(ctx echo.Context, entityType string) error { func (h *Handler) Create(ctx echo.Context, entityType EntityType) error {
switch entityType { switch entityType.(type) {
case "PasswordToken": case *PasswordToken:
return h.PasswordTokenCreate(ctx) return h.PasswordTokenCreate(ctx)
case "User": case *User:
return h.UserCreate(ctx) return h.UserCreate(ctx)
default: default:
return fmt.Errorf("unsupported entity type: %s", entityType) return fmt.Errorf("unsupported entity type: %s", entityType)
} }
} }
func (h *Handler) Get(ctx echo.Context, entityType string, id int) (url.Values, error) { func (h *Handler) Get(ctx echo.Context, entityType EntityType, id int) (url.Values, error) {
switch entityType { switch entityType.(type) {
case "PasswordToken": case *PasswordToken:
return h.PasswordTokenGet(ctx, id) return h.PasswordTokenGet(ctx, id)
case "User": case *User:
return h.UserGet(ctx, id) return h.UserGet(ctx, id)
default: default:
return nil, fmt.Errorf("unsupported entity type: %s", entityType) return nil, fmt.Errorf("unsupported entity type: %s", entityType)
} }
} }
func (h *Handler) Delete(ctx echo.Context, entityType string, id int) error { func (h *Handler) Delete(ctx echo.Context, entityType EntityType, id int) error {
switch entityType { switch entityType.(type) {
case "PasswordToken": case *PasswordToken:
return h.PasswordTokenDelete(ctx, id) return h.PasswordTokenDelete(ctx, id)
case "User": case *User:
return h.UserDelete(ctx, id) return h.UserDelete(ctx, id)
default: default:
return fmt.Errorf("unsupported entity type: %s", entityType) return fmt.Errorf("unsupported entity type: %s", entityType)
} }
} }
func (h *Handler) Update(ctx echo.Context, entityType string, id int) error { func (h *Handler) Update(ctx echo.Context, entityType EntityType, id int) error {
switch entityType { switch entityType.(type) {
case "PasswordToken": case *PasswordToken:
return h.PasswordTokenUpdate(ctx, id) return h.PasswordTokenUpdate(ctx, id)
case "User": case *User:
return h.UserUpdate(ctx, id) return h.UserUpdate(ctx, id)
default: default:
return fmt.Errorf("unsupported entity type: %s", entityType) return fmt.Errorf("unsupported entity type: %s", entityType)
} }
} }
func (h *Handler) List(ctx echo.Context, entityType string) (*EntityList, error) { func (h *Handler) List(ctx echo.Context, entityType EntityType) (*EntityList, error) {
switch entityType { switch entityType.(type) {
case "PasswordToken": case *PasswordToken:
return h.PasswordTokenList(ctx) return h.PasswordTokenList(ctx)
case "User": case *User:
return h.UserList(ctx) return h.UserList(ctx)
default: default:
return nil, fmt.Errorf("unsupported entity type: %s", entityType) return nil, fmt.Errorf("unsupported entity type: %s", entityType)

101
ent/admin/schema.go Normal file
View file

@ -0,0 +1,101 @@
// Code generated by ent, DO NOT EDIT.
package admin
import (
"entgo.io/ent/schema/field"
)
type Enum struct {
Label, Value string
}
type FieldSchema struct {
Name string
Type field.Type
Optional bool
Immutable bool
Sensitive bool
Enums []Enum
}
const NamePasswordToken = "PasswordToken"
var fieldsPasswordToken = []*FieldSchema{
{
Name: "token",
Type: field.TypeString,
Optional: false,
Immutable: false,
Sensitive: true,
Enums: nil,
},
{
Name: "user_id",
Type: field.TypeInt,
Optional: false,
Immutable: false,
Sensitive: false,
Enums: nil,
},
{
Name: "created_at",
Type: field.TypeTime,
Optional: false,
Immutable: false,
Sensitive: false,
Enums: nil,
},
}
const NameUser = "User"
var fieldsUser = []*FieldSchema{
{
Name: "name",
Type: field.TypeString,
Optional: false,
Immutable: false,
Sensitive: false,
Enums: nil,
},
{
Name: "email",
Type: field.TypeString,
Optional: false,
Immutable: false,
Sensitive: false,
Enums: nil,
},
{
Name: "password",
Type: field.TypeString,
Optional: false,
Immutable: false,
Sensitive: true,
Enums: nil,
},
{
Name: "verified",
Type: field.TypeBool,
Optional: false,
Immutable: false,
Sensitive: false,
Enums: nil,
},
{
Name: "admin",
Type: field.TypeBool,
Optional: false,
Immutable: false,
Sensitive: false,
Enums: nil,
},
{
Name: "created_at",
Type: field.TypeTime,
Optional: false,
Immutable: true,
Sensitive: false,
Enums: nil,
},
}

View file

@ -35,10 +35,10 @@
} }
} }
func (h *Handler) Create(ctx echo.Context, entityType string) error { func (h *Handler) Create(ctx echo.Context, entityType EntityType) error {
switch entityType { switch entityType.(type) {
{{- range $n := $.Nodes }} {{- range $n := $.Nodes }}
case "{{ $n.Name }}": case *{{ $n.Name }}:
return h.{{ $n.Name }}Create(ctx) return h.{{ $n.Name }}Create(ctx)
{{- end }} {{- end }}
default: default:
@ -46,10 +46,10 @@
} }
} }
func (h *Handler) Get(ctx echo.Context, entityType string, id int) (url.Values, error) { func (h *Handler) Get(ctx echo.Context, entityType EntityType, id int) (url.Values, error) {
switch entityType { switch entityType.(type) {
{{- range $n := $.Nodes }} {{- range $n := $.Nodes }}
case "{{ $n.Name }}": case *{{ $n.Name }}:
return h.{{ $n.Name }}Get(ctx, id) return h.{{ $n.Name }}Get(ctx, id)
{{- end }} {{- end }}
default: default:
@ -57,10 +57,10 @@
} }
} }
func (h *Handler) Delete(ctx echo.Context, entityType string, id int) error { func (h *Handler) Delete(ctx echo.Context, entityType EntityType, id int) error {
switch entityType { switch entityType.(type) {
{{- range $n := $.Nodes }} {{- range $n := $.Nodes }}
case "{{ $n.Name }}": case *{{ $n.Name }}:
return h.{{ $n.Name }}Delete(ctx, id) return h.{{ $n.Name }}Delete(ctx, id)
{{- end }} {{- end }}
default: default:
@ -68,10 +68,10 @@
} }
} }
func (h *Handler) Update(ctx echo.Context, entityType string, id int) error { func (h *Handler) Update(ctx echo.Context, entityType EntityType, id int) error {
switch entityType { switch entityType.(type) {
{{- range $n := $.Nodes }} {{- range $n := $.Nodes }}
case "{{ $n.Name }}": case *{{ $n.Name }}:
return h.{{ $n.Name }}Update(ctx, id) return h.{{ $n.Name }}Update(ctx, id)
{{- end }} {{- end }}
default: default:
@ -79,10 +79,10 @@
} }
} }
func (h *Handler) List(ctx echo.Context, entityType string) (*EntityList, error) { func (h *Handler) List(ctx echo.Context, entityType EntityType) (*EntityList, error) {
switch entityType { switch entityType.(type) {
{{- range $n := $.Nodes }} {{- range $n := $.Nodes }}
case "{{ $n.Name }}": case *{{ $n.Name }}:
return h.{{ $n.Name }}List(ctx) return h.{{ $n.Name }}List(ctx)
{{- end }} {{- end }}
default: default:

View file

@ -0,0 +1,54 @@
{{/* Tell Intellij/GoLand to enable the autocompletion based on the *gen.Graph type. */}}
{{/* gotype: entgo.io/ent/entc/gen.Graph */}}
{{ define "admin/schema" }}
// Code generated by ent, DO NOT EDIT.
package admin
import (
"entgo.io/ent/schema/field"
)
type Enum struct {
Label, Value string
}
type FieldSchema struct {
Name string
Type field.Type
Optional bool
Immutable bool
Sensitive bool
Enums []Enum
}
{{- range $n := $.Nodes }}
const Name{{ $n.Name }} = "{{ $n.Name }}"
var fields{{ $n.Name }} = []*FieldSchema{
{{- range $f := $n.Fields }}
{
Name: "{{ $f.Name }}",
Type: field.{{ $f.Type.Type.ConstName }},
Optional: {{ $f.Optional }},
Immutable: {{ $f.Immutable }},
Sensitive: {{ $f.Sensitive }},
{{- if len $f.Enums }}
Enums: []Enum{
{{- range $e := $f.Enums }}
{
Label: "{{ $e.Label }}",
Value: "{{ $e.Value }}",
},
{{- end }}
},
{{- else }}
Enums: nil,
{{- end }}
},
{{- end }}
}
{{ end }}
{{ end }}

View file

@ -11,8 +11,27 @@
{{ fieldName $f.Name }} {{ if (fieldIsPointer $f) }}*{{ end }}{{ $f.Type }} `form:"{{ $f.Name }}"` {{ fieldName $f.Name }} {{ if (fieldIsPointer $f) }}*{{ end }}{{ $f.Type }} `form:"{{ $f.Name }}"`
{{- end }} {{- end }}
} }
func (e *{{ $n.Name }}) GetName() string {
return Name{{ $n.Name }}
}
func (e *{{ $n.Name }}) GetSchema() []*FieldSchema {
return fields{{ $n.Name }}
}
{{ end }} {{ end }}
type EntityType interface {
GetName() string
GetSchema() []*FieldSchema
}
var entityTypes = []EntityType{
{{- range $n := $.Nodes }}
&{{ $n.Name }}{},
{{- end }}
}
type EntityList struct { type EntityList struct {
Columns []string Columns []string
Entities []EntityValues Entities []EntityValues
@ -31,12 +50,7 @@
TimeFormat string TimeFormat string
} }
func GetEntityTypeNames() []string { func GetEntityTypes() []EntityType {
return []string{ return entityTypes
{{- range $n := $.Nodes }}
"{{ $n.Name }}",
{{- end }}
}
} }
{{ end }} {{ end }}

View file

@ -9,6 +9,14 @@ type PasswordToken struct {
CreatedAt *time.Time `form:"created_at"` CreatedAt *time.Time `form:"created_at"`
} }
func (e *PasswordToken) GetName() string {
return NamePasswordToken
}
func (e *PasswordToken) GetSchema() []*FieldSchema {
return fieldsPasswordToken
}
type User struct { type User struct {
Name string `form:"name"` Name string `form:"name"`
Email string `form:"email"` Email string `form:"email"`
@ -18,6 +26,24 @@ type User struct {
CreatedAt *time.Time `form:"created_at"` CreatedAt *time.Time `form:"created_at"`
} }
func (e *User) GetName() string {
return NameUser
}
func (e *User) GetSchema() []*FieldSchema {
return fieldsUser
}
type EntityType interface {
GetName() string
GetSchema() []*FieldSchema
}
var entityTypes = []EntityType{
&PasswordToken{},
&User{},
}
type EntityList struct { type EntityList struct {
Columns []string Columns []string
Entities []EntityValues Entities []EntityValues
@ -36,9 +62,6 @@ type HandlerConfig struct {
TimeFormat string TimeFormat string
} }
func GetEntityTypeNames() []string { func GetEntityTypes() []EntityType {
return []string{ return entityTypes
"PasswordToken",
"User",
}
} }

View file

@ -7,8 +7,6 @@ import (
"strings" "strings"
"time" "time"
"entgo.io/ent/entc/gen"
"entgo.io/ent/entc/load"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/mikestefanello/backlite/ui" "github.com/mikestefanello/backlite/ui"
"github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/ent"
@ -25,7 +23,6 @@ import (
type Admin struct { type Admin struct {
orm *ent.Client orm *ent.Client
graph *gen.Graph
admin *admin.Handler admin *admin.Handler
backlite *ui.Handler backlite *ui.Handler
} }
@ -36,7 +33,6 @@ func init() {
func (h *Admin) Init(c *services.Container) error { func (h *Admin) Init(c *services.Container) error {
var err error var err error
h.graph = c.Graph
h.orm = c.ORM h.orm = c.ORM
h.admin = admin.NewHandler(h.orm, admin.HandlerConfig{ h.admin = admin.NewHandler(h.orm, admin.HandlerConfig{
ItemsPerPage: 25, ItemsPerPage: 25,
@ -56,22 +52,22 @@ func (h *Admin) Routes(g *echo.Group) {
ag := g.Group("/admin", middleware.RequireAdmin) ag := g.Group("/admin", middleware.RequireAdmin)
entities := ag.Group("/entity") entities := ag.Group("/entity")
for _, n := range h.graph.Nodes { for _, n := range admin.GetEntityTypes() {
ng := entities.Group(fmt.Sprintf("/%s", strings.ToLower(n.Name))) ng := entities.Group(fmt.Sprintf("/%s", strings.ToLower(n.GetName())))
ng.GET("", h.EntityList(n)). ng.GET("", h.EntityList(n)).
Name = routenames.AdminEntityList(n.Name) Name = routenames.AdminEntityList(n.GetName())
ng.GET("/add", h.EntityAdd(n)). ng.GET("/add", h.EntityAdd(n)).
Name = routenames.AdminEntityAdd(n.Name) Name = routenames.AdminEntityAdd(n.GetName())
ng.POST("/add", h.EntityAddSubmit(n)). ng.POST("/add", h.EntityAddSubmit(n)).
Name = routenames.AdminEntityAddSubmit(n.Name) Name = routenames.AdminEntityAddSubmit(n.GetName())
ng.GET("/:id/edit", h.EntityEdit(n), h.middlewareEntityLoad(n)). ng.GET("/:id/edit", h.EntityEdit(n), h.middlewareEntityLoad(n)).
Name = routenames.AdminEntityEdit(n.Name) Name = routenames.AdminEntityEdit(n.GetName())
ng.POST("/:id/edit", h.EntityEditSubmit(n), h.middlewareEntityLoad(n)). ng.POST("/:id/edit", h.EntityEditSubmit(n), h.middlewareEntityLoad(n)).
Name = routenames.AdminEntityEditSubmit(n.Name) Name = routenames.AdminEntityEditSubmit(n.GetName())
ng.GET("/:id/delete", h.EntityDelete(n), h.middlewareEntityLoad(n)). ng.GET("/:id/delete", h.EntityDelete(n), h.middlewareEntityLoad(n)).
Name = routenames.AdminEntityDelete(n.Name) Name = routenames.AdminEntityDelete(n.GetName())
ng.POST("/:id/delete", h.EntityDeleteSubmit(n), h.middlewareEntityLoad(n)). ng.POST("/:id/delete", h.EntityDeleteSubmit(n), h.middlewareEntityLoad(n)).
Name = routenames.AdminEntityDeleteSubmit(n.Name) Name = routenames.AdminEntityDeleteSubmit(n.GetName())
} }
tasks := ag.Group("/tasks") tasks := ag.Group("/tasks")
@ -84,7 +80,7 @@ func (h *Admin) Routes(g *echo.Group) {
} }
// middlewareEntityLoad is middleware to extract the entity ID and attempt to load the given entity. // middlewareEntityLoad is middleware to extract the entity ID and attempt to load the given entity.
func (h *Admin) middlewareEntityLoad(n *gen.Type) echo.MiddlewareFunc { func (h *Admin) middlewareEntityLoad(n admin.EntityType) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
id, err := strconv.Atoi(ctx.Param("id")) id, err := strconv.Atoi(ctx.Param("id"))
@ -92,7 +88,7 @@ func (h *Admin) middlewareEntityLoad(n *gen.Type) echo.MiddlewareFunc {
return echo.NewHTTPError(http.StatusBadRequest, "invalid entity ID") return echo.NewHTTPError(http.StatusBadRequest, "invalid entity ID")
} }
entity, err := h.admin.Get(ctx, n.Name, id) entity, err := h.admin.Get(ctx, n, id)
switch { switch {
case err == nil: case err == nil:
ctx.Set(context.AdminEntityIDKey, id) ctx.Set(context.AdminEntityIDKey, id)
@ -107,100 +103,91 @@ func (h *Admin) middlewareEntityLoad(n *gen.Type) echo.MiddlewareFunc {
} }
} }
func (h *Admin) EntityList(n *gen.Type) echo.HandlerFunc { func (h *Admin) EntityList(n admin.EntityType) echo.HandlerFunc {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
list, err := h.admin.List(ctx, n.Name) list, err := h.admin.List(ctx, n)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err) return echo.NewHTTPError(http.StatusInternalServerError, err)
} }
return pages.AdminEntityList(ctx, n.Name, list) return pages.AdminEntityList(ctx, n, list)
} }
} }
func (h *Admin) EntityAdd(n *gen.Type) echo.HandlerFunc { func (h *Admin) EntityAdd(n admin.EntityType) echo.HandlerFunc {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
return pages.AdminEntityInput(ctx, h.getEntitySchema(n), nil) return pages.AdminEntityInput(ctx, n, nil)
} }
} }
func (h *Admin) EntityAddSubmit(n *gen.Type) echo.HandlerFunc { func (h *Admin) EntityAddSubmit(n admin.EntityType) echo.HandlerFunc {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
err := h.admin.Create(ctx, n.Name) err := h.admin.Create(ctx, n)
if err != nil { if err != nil {
msg.Error(ctx, err.Error()) msg.Error(ctx, err.Error())
return h.EntityAdd(n)(ctx) return h.EntityAdd(n)(ctx)
} }
msg.Success(ctx, fmt.Sprintf("Successfully added %s.", n.Name)) msg.Success(ctx, fmt.Sprintf("Successfully added %s.", n.GetName()))
return redirect. return redirect.
New(ctx). New(ctx).
Route(routenames.AdminEntityList(n.Name)). Route(routenames.AdminEntityList(n.GetName())).
StatusCode(http.StatusFound). StatusCode(http.StatusFound).
Go() Go()
} }
} }
func (h *Admin) EntityEdit(n *gen.Type) echo.HandlerFunc { func (h *Admin) EntityEdit(n admin.EntityType) echo.HandlerFunc {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
v := ctx.Get(context.AdminEntityKey).(map[string][]string) v := ctx.Get(context.AdminEntityKey).(map[string][]string)
return pages.AdminEntityInput(ctx, h.getEntitySchema(n), v) return pages.AdminEntityInput(ctx, n, v)
} }
} }
func (h *Admin) EntityEditSubmit(n *gen.Type) echo.HandlerFunc { func (h *Admin) EntityEditSubmit(n admin.EntityType) echo.HandlerFunc {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
id := ctx.Get(context.AdminEntityIDKey).(int) id := ctx.Get(context.AdminEntityIDKey).(int)
err := h.admin.Update(ctx, n.Name, id) err := h.admin.Update(ctx, n, id)
if err != nil { if err != nil {
msg.Error(ctx, err.Error()) msg.Error(ctx, err.Error())
return h.EntityEdit(n)(ctx) return h.EntityEdit(n)(ctx)
} }
msg.Success(ctx, fmt.Sprintf("Updated %s.", n.Name)) msg.Success(ctx, fmt.Sprintf("Updated %s.", n.GetName()))
return redirect. return redirect.
New(ctx). New(ctx).
Route(routenames.AdminEntityList(n.Name)). Route(routenames.AdminEntityList(n.GetName())).
StatusCode(http.StatusFound). StatusCode(http.StatusFound).
Go() Go()
} }
} }
func (h *Admin) EntityDelete(n *gen.Type) echo.HandlerFunc { func (h *Admin) EntityDelete(n admin.EntityType) echo.HandlerFunc {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
return pages.AdminEntityDelete(ctx, n.Name) return pages.AdminEntityDelete(ctx, n)
} }
} }
func (h *Admin) EntityDeleteSubmit(n *gen.Type) echo.HandlerFunc { func (h *Admin) EntityDeleteSubmit(n admin.EntityType) echo.HandlerFunc {
return func(ctx echo.Context) error { return func(ctx echo.Context) error {
id := ctx.Get(context.AdminEntityIDKey).(int) id := ctx.Get(context.AdminEntityIDKey).(int)
if err := h.admin.Delete(ctx, n.Name, id); err != nil { if err := h.admin.Delete(ctx, n, id); err != nil {
msg.Error(ctx, err.Error()) msg.Error(ctx, err.Error())
return h.EntityDelete(n)(ctx) return h.EntityDelete(n)(ctx)
} }
msg.Success(ctx, fmt.Sprintf("Successfully deleted %s (ID %d).", n.Name, id)) msg.Success(ctx, fmt.Sprintf("Successfully deleted %s (ID %d).", n.GetName(), id))
return redirect. return redirect.
New(ctx). New(ctx).
Route(routenames.AdminEntityList(n.Name)). Route(routenames.AdminEntityList(n.GetName())).
StatusCode(http.StatusFound). StatusCode(http.StatusFound).
Go() Go()
} }
} }
func (h *Admin) getEntitySchema(n *gen.Type) *load.Schema {
for _, s := range h.graph.Schemas {
if s.Name == n.Name {
return s
}
}
return nil
}
func (h *Admin) Backlite(handler func(http.ResponseWriter, *http.Request) error) echo.HandlerFunc { func (h *Admin) Backlite(handler func(http.ResponseWriter, *http.Request) error) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
if id := c.Param("id"); id != "" { if id := c.Param("id"); id != "" {

View file

@ -7,14 +7,9 @@ import (
"log/slog" "log/slog"
"math/rand" "math/rand"
"os" "os"
"path"
"path/filepath"
"runtime"
"strings" "strings"
entsql "entgo.io/ent/dialect/sql" entsql "entgo.io/ent/dialect/sql"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/mikestefanello/backlite" "github.com/mikestefanello/backlite"
@ -51,9 +46,6 @@ type Container struct {
// ORM stores a client to the ORM. // ORM stores a client to the ORM.
ORM *ent.Client ORM *ent.Client
// Graph is the entity graph defined by your Ent schema.
Graph *gen.Graph
// Mail stores an email sending client. // Mail stores an email sending client.
Mail *MailClient Mail *MailClient
@ -192,16 +184,6 @@ func (c *Container) initORM() {
if err := c.ORM.Schema.Create(context.Background()); err != nil { if err := c.ORM.Schema.Create(context.Background()); err != nil {
panic(err) panic(err)
} }
// Load the graph.
_, b, _, _ := runtime.Caller(0)
d := path.Join(path.Dir(b))
p := filepath.Join(filepath.Dir(d), "../ent/schema")
g, err := entc.LoadGraph(p, &gen.Config{})
if err != nil {
panic(err)
}
c.Graph = g
} }
// initAuth initializes the authentication client. // initAuth initializes the authentication client.

View file

@ -4,7 +4,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"entgo.io/ent/entc/load"
"entgo.io/ent/schema/field" "entgo.io/ent/schema/field"
"github.com/mikestefanello/pagoda/ent/admin" "github.com/mikestefanello/pagoda/ent/admin"
"github.com/mikestefanello/pagoda/pkg/routenames" "github.com/mikestefanello/pagoda/pkg/routenames"
@ -14,7 +13,7 @@ import (
. "maragu.dev/gomponents/html" . "maragu.dev/gomponents/html"
) )
func AdminEntity(r *ui.Request, schema *load.Schema, values url.Values) Node { func AdminEntity(r *ui.Request, entityType admin.EntityType, values url.Values) Node {
// TODO inline validation? // TODO inline validation?
isNew := values == nil isNew := values == nil
nodes := make(Group, 0) nodes := make(Group, 0)
@ -34,13 +33,13 @@ func AdminEntity(r *ui.Request, schema *load.Schema, values url.Values) Node {
} }
// Attempt to add form elements for all editable entity fields. // Attempt to add form elements for all editable entity fields.
for _, f := range schema.Fields { for _, f := range entityType.GetSchema() {
// TODO cardinality? // TODO cardinality?
if !isNew && f.Immutable { if !isNew && f.Immutable {
continue continue
} }
switch f.Info.Type { switch f.Type {
case field.TypeString: case field.TypeString:
p := InputFieldParams{ p := InputFieldParams{
Name: f.Name, Name: f.Name,
@ -93,8 +92,8 @@ func AdminEntity(r *ui.Request, schema *load.Schema, values url.Values) Node {
} }
for _, enum := range f.Enums { for _, enum := range f.Enums {
options = append(options, Choice{ options = append(options, Choice{
Label: enum.V, Label: enum.Label,
Value: enum.V, Value: enum.Value,
}) })
} }
nodes = append(nodes, SelectList(OptionsParams{ nodes = append(nodes, SelectList(OptionsParams{
@ -116,7 +115,7 @@ func AdminEntity(r *ui.Request, schema *load.Schema, values url.Values) Node {
FormButton(ColorPrimary, "Submit"), FormButton(ColorPrimary, "Submit"),
ButtonLink( ButtonLink(
ColorNone, ColorNone,
r.Path(routenames.AdminEntityList(schema.Name)), r.Path(routenames.AdminEntityList(entityType.GetName())),
"Cancel", "Cancel",
), ),
), ),

View file

@ -3,6 +3,7 @@ package forms
import ( import (
"net/http" "net/http"
"github.com/mikestefanello/pagoda/ent/admin"
"github.com/mikestefanello/pagoda/pkg/routenames" "github.com/mikestefanello/pagoda/pkg/routenames"
"github.com/mikestefanello/pagoda/pkg/ui" "github.com/mikestefanello/pagoda/pkg/ui"
. "github.com/mikestefanello/pagoda/pkg/ui/components" . "github.com/mikestefanello/pagoda/pkg/ui/components"
@ -10,17 +11,17 @@ import (
. "maragu.dev/gomponents/html" . "maragu.dev/gomponents/html"
) )
func AdminEntityDelete(r *ui.Request, entityTypeName string) Node { func AdminEntityDelete(r *ui.Request, entityType admin.EntityType) Node {
return Form( return Form(
Method(http.MethodPost), Method(http.MethodPost),
P( P(
Textf("Are you sure you want to delete this %s?", entityTypeName), Textf("Are you sure you want to delete this %s?", entityType.GetName()),
), ),
ControlGroup( ControlGroup(
FormButton(ColorError, "Delete"), FormButton(ColorError, "Delete"),
ButtonLink( ButtonLink(
ColorNone, ColorNone,
r.Path(routenames.AdminEntityList(entityTypeName)), r.Path(routenames.AdminEntityList(entityType.GetName())),
"Cancel", "Cancel",
), ),
), ),

View file

@ -121,10 +121,12 @@ func sidebarMenu(r *ui.Request) Node {
} }
adminSubMenu := func() Node { adminSubMenu := func() Node {
entityTypeNames := admin.GetEntityTypeNames() entityTypeLinks := make(Group, len(admin.GetEntityTypes()))
entityTypeLinks := make(Group, len(entityTypeNames)) for _, n := range admin.GetEntityTypes() {
for _, n := range entityTypeNames { entityTypeLinks = append(
entityTypeLinks = append(entityTypeLinks, MenuLink(r, icons.PencilSquare(), n, routenames.AdminEntityList(n))) entityTypeLinks,
MenuLink(r, icons.PencilSquare(), n.GetName(), routenames.AdminEntityList(n.GetName())),
)
} }
return Group{ return Group{

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"entgo.io/ent/entc/load"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/ent/admin" "github.com/mikestefanello/pagoda/ent/admin"
"github.com/mikestefanello/pagoda/pkg/routenames" "github.com/mikestefanello/pagoda/pkg/routenames"
@ -16,37 +15,37 @@ import (
. "maragu.dev/gomponents/html" . "maragu.dev/gomponents/html"
) )
func AdminEntityDelete(ctx echo.Context, entityTypeName string) error { func AdminEntityDelete(ctx echo.Context, entityType admin.EntityType) error {
r := ui.NewRequest(ctx) r := ui.NewRequest(ctx)
r.Title = fmt.Sprintf("Delete %s", entityTypeName) r.Title = fmt.Sprintf("Delete %s", entityType.GetName())
return r.Render( return r.Render(
layouts.Primary, layouts.Primary,
forms.AdminEntityDelete(r, entityTypeName), forms.AdminEntityDelete(r, entityType),
) )
} }
func AdminEntityInput(ctx echo.Context, schema *load.Schema, values url.Values) error { func AdminEntityInput(ctx echo.Context, entityType admin.EntityType, values url.Values) error {
r := ui.NewRequest(ctx) r := ui.NewRequest(ctx)
if values == nil { if values == nil {
r.Title = fmt.Sprintf("Add %s", schema.Name) r.Title = fmt.Sprintf("Add %s", entityType.GetName())
} else { } else {
r.Title = fmt.Sprintf("Edit %s", schema.Name) r.Title = fmt.Sprintf("Edit %s", entityType.GetName())
} }
return r.Render( return r.Render(
layouts.Primary, layouts.Primary,
forms.AdminEntity(r, schema, values), forms.AdminEntity(r, entityType, values),
) )
} }
func AdminEntityList( func AdminEntityList(
ctx echo.Context, ctx echo.Context,
entityTypeName string, entityType admin.EntityType,
entityList *admin.EntityList, entityList *admin.EntityList,
) error { ) error {
r := ui.NewRequest(ctx) r := ui.NewRequest(ctx)
r.Title = entityTypeName r.Title = entityType.GetName()
genHeader := func() Node { genHeader := func() Node {
g := make(Group, 0, len(entityList.Columns)+2) g := make(Group, 0, len(entityList.Columns)+2)
@ -68,13 +67,13 @@ func AdminEntityList(
Td( Td(
ButtonLink( ButtonLink(
ColorInfo, ColorInfo,
r.Path(routenames.AdminEntityEdit(entityTypeName), row.ID), r.Path(routenames.AdminEntityEdit(entityType.GetName()), row.ID),
"Edit", "Edit",
), ),
Span(Class("mr-2")), Span(Class("mr-2")),
ButtonLink( ButtonLink(
ColorError, ColorError,
r.Path(routenames.AdminEntityDelete(entityTypeName), row.ID), r.Path(routenames.AdminEntityDelete(entityType.GetName()), row.ID),
"Delete", "Delete",
), ),
), ),
@ -95,8 +94,8 @@ func AdminEntityList(
Class("form-control mb-2"), Class("form-control mb-2"),
ButtonLink( ButtonLink(
ColorAccent, ColorAccent,
r.Path(routenames.AdminEntityAdd(entityTypeName)), r.Path(routenames.AdminEntityAdd(entityType.GetName())),
fmt.Sprintf("Add %s", entityTypeName), fmt.Sprintf("Add %s", entityType.GetName()),
), ),
), ),
Table( Table(
@ -108,7 +107,7 @@ func AdminEntityList(
), ),
Pager( Pager(
entityList.Page, entityList.Page,
r.Path(routenames.AdminEntityAdd(entityTypeName)), r.Path(routenames.AdminEntityAdd(entityType.GetName())),
entityList.HasNextPage, entityList.HasNextPage,
"", "",
), ),