Use custom ent codegen plugin for admin.

This commit is contained in:
mikestefanello 2025-04-08 14:02:45 -04:00
parent 9139942794
commit ce9b58bf4a
33 changed files with 750 additions and 13452 deletions

View file

@ -6,19 +6,16 @@ import (
"net/http"
"strconv"
"strings"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/entc/gen"
"entgo.io/ent/entc/load"
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/ent"
"github.com/mikestefanello/pagoda/ent/ogent"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/user"
"github.com/mikestefanello/pagoda/ent/admin"
"github.com/mikestefanello/pagoda/pkg/msg"
"github.com/mikestefanello/pagoda/pkg/pager"
"github.com/mikestefanello/pagoda/pkg/redirect"
"github.com/mikestefanello/pagoda/pkg/routenames"
"github.com/mikestefanello/pagoda/pkg/services"
"github.com/mikestefanello/pagoda/pkg/ui/pages"
)
@ -28,9 +25,10 @@ const entityContextKey = "admin:entity"
const entityIDContextKey = "admin:entity_id"
type Admin struct {
orm *ent.Client
graph *gen.Graph
ogent *ogent.OgentHandler
orm *ent.Client
graph *gen.Graph
admin *admin.Handler
itemsPerPage int
}
func init() {
@ -38,9 +36,10 @@ func init() {
}
func (h *Admin) Init(c *services.Container) error {
h.itemsPerPage = 25
h.graph = c.Graph
h.orm = c.ORM
h.ogent = ogent.NewOgentHandler(h.orm)
h.admin = admin.NewHandler(h.orm, h.itemsPerPage)
return nil
}
@ -48,21 +47,24 @@ func (h *Admin) Routes(g *echo.Group) {
// TODO admin user status middleware
entities := g.Group("/admin/content")
for _, p := range h.getEntityPlugins() {
pg := entities.Group(fmt.Sprintf("/%s", strings.ToLower(p.ID)))
pg.GET("", h.EntityList(p)).Name = p.RouteNameList()
pg.POST("", h.EntityList(p)).Name = p.RouteNameListSubmit()
pg.GET("/add", h.EntityAdd(p)).Name = p.RouteNameAdd()
pg.POST("/add", h.EntityAddSubmit(p)).Name = p.RouteNameAddSubmit()
pg.GET("/:id/edit", h.EntityEdit(p), h.entityPluginMiddleware(p)).Name = p.RouteNameEdit()
pg.POST("/:id/edit", h.EntityEditSubmit(p), h.entityPluginMiddleware(p)).Name = p.RouteNameEditSubmit()
pg.GET("/:id/delete", h.EntityDelete(p), h.entityPluginMiddleware(p)).Name = p.RouteNameDelete()
pg.POST("/:id/delete", h.EntityDeleteSubmit(p), h.entityPluginMiddleware(p)).Name = p.RouteNameDeleteSubmit()
// TODO: can we generate something we can loop instead?
for _, n := range h.graph.Nodes {
ng := entities.Group(fmt.Sprintf("/%s", strings.ToLower(n.Name)))
ng.GET("", h.EntityList(n)).Name = routenames.AdminEntityList(n.Name)
ng.POST("", h.EntityList(n)).Name = routenames.AdminEntityListSubmit(n.Name)
ng.GET("/add", h.EntityAdd(n)).Name = routenames.AdminEntityAdd(n.Name)
ng.POST("/add", h.EntityAddSubmit(n)).Name = routenames.AdminEntityAddSubmit(n.Name)
//ng.GET("/:id/edit", h.EntityEdit(n), h.entityPluginMiddleware(n.Name)).Name = RouteNameEdit(n.Name)
//ng.POST("/:id/edit", h.EntityEditSubmit(n), h.entityPluginMiddleware(n.Name)).Name = RouteNameEditSubmit(n.Name)
ng.GET("/:id/delete", h.EntityDelete(n), h.middlewareEntityLoad(n)).
Name = routenames.AdminEntityDelete(n.Name)
ng.POST("/:id/delete", h.EntityDeleteSubmit(n), h.middlewareEntityLoad(n)).
Name = routenames.AdminEntityDeleteSubmit(n.Name)
}
}
// TODO, maybe this can be used outside of admin stuff as well?
func (h *Admin) entityPluginMiddleware(plugin AdminEntityPlugin) echo.MiddlewareFunc {
func (h *Admin) middlewareEntityLoad(n *gen.Type) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
id, err := strconv.Atoi(ctx.Param("id"))
@ -70,11 +72,11 @@ func (h *Admin) entityPluginMiddleware(plugin AdminEntityPlugin) echo.Middleware
return echo.NewHTTPError(http.StatusBadRequest, "invalid entity ID")
}
entity, err := plugin.Load(ctx, h.orm, id)
err = h.admin.Get(ctx, n.Name, id)
switch {
case err == nil:
ctx.Set(entityIDContextKey, id)
ctx.Set(entityContextKey, entity)
//ctx.Set(entityContextKey, entity) // TODO
return next(ctx)
case errors.Is(err, new(ent.NotFoundError)):
return echo.NewHTTPError(http.StatusNotFound, "entity not found")
@ -85,228 +87,85 @@ func (h *Admin) entityPluginMiddleware(plugin AdminEntityPlugin) echo.Middleware
}
}
func (h *Admin) EntityList(p AdminEntityPlugin) echo.HandlerFunc {
func (h *Admin) EntityList(n *gen.Type) echo.HandlerFunc {
return func(ctx echo.Context) error {
var err error
pgr := pager.NewPager(ctx, 25)
params := pages.AdminEntityListParams{
Title: p.LabelPlural,
Headers: p.Heading,
EditRoute: p.RouteNameEdit(), // todo remove, pass in plugin
DeleteRoute: p.RouteNameDelete(), // todo remove, pass in plugin
Pager: pgr,
}
params.Rows, err = p.List(ctx, h.orm, pgr)
list, err := h.admin.List(ctx, n.Name)
if err != nil {
return fail(err, fmt.Sprintf("failed to query %s", p.ID))
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return pages.AdminEntityList(ctx, params)
return pages.AdminEntityList(ctx, pages.AdminEntityListParams{
EntityType: n,
EntityList: list,
Pager: pager.NewPager(ctx, h.itemsPerPage),
})
}
}
func (h *Admin) EntityAdd(p AdminEntityPlugin) echo.HandlerFunc {
func (h *Admin) EntityAdd(n *gen.Type) echo.HandlerFunc {
return func(ctx echo.Context) error {
var schema *load.Schema
for _, s := range h.graph.Schemas {
if s.Name == p.ID {
schema = s
}
}
return pages.AdminEntityAdd(ctx, schema)
return pages.AdminEntityAdd(ctx, h.getEntitySchema(n))
}
}
func (h *Admin) EntityAddSubmit(p AdminEntityPlugin) echo.HandlerFunc {
func (h *Admin) EntityAddSubmit(n *gen.Type) echo.HandlerFunc {
return func(ctx echo.Context) error {
var v ogent.CreatePasswordTokenReq // TODO type
err := ctx.Bind(&v)
if err != nil {
return fail(err, fmt.Sprintf("failed to bind create password token request body"))
}
res, err := h.ogent.CreatePasswordToken(ctx.Request().Context(), &v)
err := h.admin.Create(ctx, n.Name)
if err != nil {
msg.Danger(ctx, err.Error())
return h.EntityAdd(p)(ctx)
// TODO : hold state
return h.EntityAdd(n)(ctx)
}
fmt.Printf("%+v\n", res)
msg.Success(ctx, fmt.Sprintf("Successfully added %s.", strings.ToLower(p.Label)))
msg.Success(ctx, fmt.Sprintf("Successfully added %s.", n.Name))
return redirect.
New(ctx).
Route(p.RouteNameList()).
Route(routenames.AdminEntityList(n.Name)).
Go()
}
}
func (h *Admin) EntityEdit(p AdminEntityPlugin) echo.HandlerFunc {
//
//func (h *Admin) EntityEdit(p AdminEntityPlugin) echo.HandlerFunc {
// return func(ctx echo.Context) error {
// return nil
// }
//}
//
//func (h *Admin) EntityEditSubmit(p AdminEntityPlugin) echo.HandlerFunc {
// return func(ctx echo.Context) error {
// return nil
// }
//}
//
func (h *Admin) EntityDelete(n *gen.Type) echo.HandlerFunc {
return func(ctx echo.Context) error {
return nil
return pages.AdminEntityDelete(ctx, n.Name)
}
}
func (h *Admin) EntityEditSubmit(p AdminEntityPlugin) echo.HandlerFunc {
return func(ctx echo.Context) error {
return nil
}
}
func (h *Admin) EntityDelete(p AdminEntityPlugin) echo.HandlerFunc {
return func(ctx echo.Context) error {
return pages.AdminEntityDelete(ctx)
}
}
func (h *Admin) EntityDeleteSubmit(p AdminEntityPlugin) echo.HandlerFunc {
func (h *Admin) EntityDeleteSubmit(n *gen.Type) echo.HandlerFunc {
return func(ctx echo.Context) error {
id := ctx.Get(entityIDContextKey).(int)
if err := p.Delete(ctx, h.orm, id); err != nil {
return fail(err, fmt.Sprintf("failed to delete %s (ID: %d)", p.ID, id))
if err := h.admin.Delete(ctx, n.Name, id); err != nil {
return fail(err, fmt.Sprintf("failed to delete %s (ID: %d)", n.Name, id))
}
msg.Success(ctx, fmt.Sprintf("Successfully deleted %s.", strings.ToLower(p.Label)))
msg.Success(ctx, fmt.Sprintf("Successfully deleted %s ID %d.", n.Name, id))
return redirect.
New(ctx).
Route(p.RouteNameList()).
Route(routenames.AdminEntityList(n.Name)).
Go()
}
}
// TODO inject orm? move to separate package?
type AdminEntityPlugin struct {
ID string
Label string
LabelPlural string
Heading []string
List func(ctx echo.Context, orm *ent.Client, pgr pager.Pager) ([]pages.AdminEntityListRow, error)
Load func(ctx echo.Context, orm *ent.Client, id int) (any, error)
Delete func(ctx echo.Context, orm *ent.Client, id int) error
}
func (p *AdminEntityPlugin) RouteNameList() string {
return fmt.Sprintf("admin:%s_list", p.ID)
}
func (p *AdminEntityPlugin) RouteNameListSubmit() string {
return fmt.Sprintf("admin:%s_list.submit", p.ID)
}
func (p *AdminEntityPlugin) RouteNameAdd() string {
return fmt.Sprintf("admin:%s_add", p.ID)
}
func (p *AdminEntityPlugin) RouteNameEdit() string {
return fmt.Sprintf("admin:%s_edit", p.ID)
}
func (p *AdminEntityPlugin) RouteNameDelete() string {
return fmt.Sprintf("admin:%s_delete", p.ID)
}
func (p *AdminEntityPlugin) RouteNameAddSubmit() string {
return fmt.Sprintf("admin:%s_add.submit", p.ID)
}
func (p *AdminEntityPlugin) RouteNameEditSubmit() string {
return fmt.Sprintf("admin:%s_edit.submit", p.ID)
}
func (p *AdminEntityPlugin) RouteNameDeleteSubmit() string {
return fmt.Sprintf("admin:%s_delete.submit", p.ID)
}
func (h *Admin) getEntityPlugins() []AdminEntityPlugin {
return []AdminEntityPlugin{
{
ID: "User",
Label: "User",
LabelPlural: "Users",
Heading: []string{
"ID",
"Name",
"Email",
"Created at",
},
List: func(ctx echo.Context, client *ent.Client, pgr pager.Pager) ([]pages.AdminEntityListRow, error) {
users, err := client.User.
Query().
Limit(pgr.ItemsPerPage).
Offset(pgr.GetOffset()).
Order(user.ByCreatedAt(sql.OrderDesc())).
All(ctx.Request().Context())
if err != nil {
return nil, err
}
rows := make([]pages.AdminEntityListRow, 0, len(users))
for _, u := range users {
rows = append(rows, pages.AdminEntityListRow{
ID: u.ID,
Columns: []string{
fmt.Sprint(u.ID),
u.Name,
u.Email,
u.CreatedAt.Format(time.RFC822),
},
})
}
return rows, nil
},
Load: func(ctx echo.Context, orm *ent.Client, id int) (any, error) {
return orm.User.Get(ctx.Request().Context(), id)
},
Delete: func(ctx echo.Context, orm *ent.Client, id int) error {
return orm.User.DeleteOneID(id).Exec(ctx.Request().Context())
},
},
{
ID: "PasswordToken",
Label: "Password token",
LabelPlural: "Password tokens",
Heading: []string{
"ID",
"Hash",
"Created at",
},
List: func(ctx echo.Context, client *ent.Client, pgr pager.Pager) ([]pages.AdminEntityListRow, error) {
tokens, err := client.PasswordToken.
Query().
Limit(pgr.ItemsPerPage).
Offset(pgr.GetOffset()).
Order(passwordtoken.ByCreatedAt(sql.OrderDesc())).
All(ctx.Request().Context())
if err != nil {
return nil, err
}
rows := make([]pages.AdminEntityListRow, 0, len(tokens))
for _, t := range tokens {
rows = append(rows, pages.AdminEntityListRow{
ID: t.ID,
Columns: []string{
fmt.Sprint(t.ID),
t.Hash,
t.CreatedAt.Format(time.RFC822),
},
})
}
return rows, nil
},
Load: func(ctx echo.Context, orm *ent.Client, id int) (any, error) {
return orm.PasswordToken.Get(ctx.Request().Context(), id)
},
Delete: func(ctx echo.Context, orm *ent.Client, id int) error {
return orm.PasswordToken.DeleteOneID(id).Exec(ctx.Request().Context())
},
},
func (h *Admin) getEntitySchema(n *gen.Type) *load.Schema {
for _, s := range h.graph.Schemas {
if s.Name == n.Name {
return s
}
}
return nil
}

View file

@ -1,5 +1,9 @@
package routenames
import (
"fmt"
)
const (
Home = "home"
About = "about"
@ -23,3 +27,35 @@ const (
Files = "files"
FilesSubmit = "files.submit"
)
func AdminEntityList(entityTypeName string) string {
return fmt.Sprintf("admin:%s_list", entityTypeName)
}
func AdminEntityListSubmit(entityTypeName string) string {
return fmt.Sprintf("admin:%s_list.submit", entityTypeName)
}
func AdminEntityAdd(entityTypeName string) string {
return fmt.Sprintf("admin:%s_add", entityTypeName)
}
func AdminEntityEdit(entityTypeName string) string {
return fmt.Sprintf("admin:%s_edit", entityTypeName)
}
func AdminEntityDelete(entityTypeName string) string {
return fmt.Sprintf("admin:%s_delete", entityTypeName)
}
func AdminEntityAddSubmit(entityTypeName string) string {
return fmt.Sprintf("admin:%s_add.submit", entityTypeName)
}
func AdminEntityEditSubmit(entityTypeName string) string {
return fmt.Sprintf("admin:%s_edit.submit", entityTypeName)
}
func AdminEntityDeleteSubmit(entityTypeName string) string {
return fmt.Sprintf("admin:%s_delete.submit", entityTypeName)
}

View file

@ -51,8 +51,8 @@ func adminMenu(r *ui.Request) Node {
),
Ul(
Class("menu-list"),
MenuLink(r, "Users", "admin:user_list"),
MenuLink(r, "Tokens", "admin:passwordtoken_list"),
MenuLink(r, "User", routenames.AdminEntityList("User")),
MenuLink(r, "PasswordToken", routenames.AdminEntityList("PasswordToken")),
),
P(
Class("menu-label"),

View file

@ -7,10 +7,13 @@ import (
"time"
"unicode"
"entgo.io/ent/entc/gen"
"entgo.io/ent/entc/load"
"entgo.io/ent/schema/field"
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/ent/admin"
"github.com/mikestefanello/pagoda/pkg/pager"
"github.com/mikestefanello/pagoda/pkg/routenames"
"github.com/mikestefanello/pagoda/pkg/ui"
. "github.com/mikestefanello/pagoda/pkg/ui/components"
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
@ -22,14 +25,14 @@ func Entity(ctx echo.Context) error {
return ui.NewRequest(ctx).Render(layouts.Admin, Div(Text("abc")))
}
func AdminEntityDelete(ctx echo.Context) error {
func AdminEntityDelete(ctx echo.Context, entityTypeName string) error {
r := ui.NewRequest(ctx)
form := Form(
Method(http.MethodPost),
H2(Text("Are you sure you want to delete this entity?")),
H2(Textf("Are you sure you want to delete this %s?", entityTypeName)),
ControlGroup(
FormButton("is-link", "Delete"),
ButtonLink("/", "is-secondary", "Cancel"),
ButtonLink(r.Path(routenames.AdminEntityList(entityTypeName)), "is-secondary", "Cancel"),
),
CSRF(r),
)
@ -39,7 +42,7 @@ func AdminEntityDelete(ctx echo.Context) error {
func AdminEntityAdd(ctx echo.Context, schema *load.Schema) error {
r := ui.NewRequest(ctx)
r.Title = fmt.Sprintf("Add %s", "entity") // TODO
r.Title = fmt.Sprintf("Add %s", schema.Name) // TODO
nodes := make(Group, 0)
@ -88,7 +91,7 @@ func AdminEntityAdd(ctx echo.Context, schema *load.Schema) error {
nodes = append(nodes, ControlGroup(
FormButton("is-primary", "Submit"),
ButtonLink("/", "is-secondary", "Cancel"), // todo
ButtonLink(r.Path(routenames.AdminEntityList(schema.Name)), "is-secondary", "Cancel"),
), CSRF(r))
return r.Render(layouts.Admin, Form(
@ -98,12 +101,9 @@ func AdminEntityAdd(ctx echo.Context, schema *load.Schema) error {
}
type AdminEntityListParams struct {
Title string
Headers []string
Rows []AdminEntityListRow
EditRoute string
DeleteRoute string
Pager pager.Pager
EntityType *gen.Type
EntityList *admin.EntityList
Pager pager.Pager
}
type AdminEntityListRow struct {
@ -113,37 +113,40 @@ type AdminEntityListRow struct {
func AdminEntityList(ctx echo.Context, params AdminEntityListParams) error {
r := ui.NewRequest(ctx)
r.Title = params.Title
r.Title = params.EntityType.Name
genHeader := func() Node {
g := make(Group, 0, len(params.Headers)+2)
for _, h := range params.Headers {
g := make(Group, 0, len(params.EntityList.Columns)+3)
g = append(g, Th(Text("ID")))
for _, h := range params.EntityList.Columns {
g = append(g, Th(Text(h)))
}
g = append(g, Th(), Th())
return g
}
genRow := func(row AdminEntityListRow) Node {
g := make(Group, 0, len(row.Columns)+2)
for _, h := range row.Columns {
genRow := func(row admin.EntityValues) Node {
g := make(Group, 0, len(row.Values)+3)
g = append(g, Th(Text(fmt.Sprint(row.ID))))
for _, h := range row.Values {
g = append(g, Td(Text(h)))
}
g = append(g,
Td(A(Href(r.Path(params.EditRoute, row.ID)), Text("Edit"))),
Td(A(Href(r.Path(params.DeleteRoute, row.ID)), Text("Delete"))),
Td(A(Href(r.Path(routenames.AdminEntityEdit(params.EntityType.Name), row.ID)), Text("Edit"))),
Td(A(Href(r.Path(routenames.AdminEntityDelete(params.EntityType.Name), row.ID)), Text("Delete"))),
)
return g
}
genRows := func() Node {
g := make(Group, 0, len(params.Rows))
for _, row := range params.Rows {
g := make(Group, 0, len(params.EntityList.Entities))
for _, row := range params.EntityList.Entities {
g = append(g, Tr(genRow(row)))
}
return g
}
// TODO pager
return r.Render(layouts.Admin, Table(
Class("table"),
THead(