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

70
ent/admin/extension.go Normal file
View file

@ -0,0 +1,70 @@
package admin
import (
"embed"
"strings"
"text/template"
"unicode"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
)
var (
//go:embed templates
templateDir embed.FS
)
type Extension struct {
entc.DefaultExtension
}
func (*Extension) Templates() []*gen.Template {
return []*gen.Template{
gen.MustParse(
gen.NewTemplate("admin").
Funcs(template.FuncMap{
"fieldName": fieldName,
"fieldLabel": fieldLabel,
}).
ParseFS(templateDir, "templates/*tmpl"),
),
}
}
func fieldName(name string) string {
if len(name) == 0 {
return name
}
parts := strings.Split(name, "_")
for i := 0; i < len(parts); i++ {
parts[i] = upperFirst(parts[i])
}
return strings.Join(parts, "")
}
func fieldLabel(name string) string {
if len(name) == 0 {
return name
}
out := strings.ReplaceAll(name, "_", " ")
return upperFirst(out)
}
func upperFirst(s string) string {
if len(s) == 0 {
return s
}
out := []rune(s)
out[0] = unicode.ToUpper(out[0])
return string(out)
}
/*
TODO:
1) How to handle fields like password that need to be transformed or omitted, etc?
2) Should we use the HTML datetime format and string fields rather than time.Time?
*/

262
ent/admin/handler.go Normal file
View file

@ -0,0 +1,262 @@
// Code generated by ent, DO NOT EDIT.
package admin
import (
"fmt"
"strconv"
"entgo.io/ent/dialect/sql"
"github.com/labstack/echo/v4"
"github.com/mikestefanello/pagoda/ent"
"github.com/mikestefanello/pagoda/ent/passwordtoken"
"github.com/mikestefanello/pagoda/ent/user"
)
type Handler struct {
client *ent.Client
itemsPerPage int
}
func NewHandler(client *ent.Client, itemsPerPage int) *Handler {
return &Handler{
client: client,
itemsPerPage: itemsPerPage,
}
}
func (h *Handler) Create(ctx echo.Context, entityType string) error {
switch entityType {
case "PasswordToken":
return h.PasswordTokenCreate(ctx)
case "User":
return h.UserCreate(ctx)
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) Get(ctx echo.Context, entityType string, id int) error {
// TODO
switch entityType {
case "PasswordToken":
return h.PasswordTokenGet(ctx, id)
case "User":
return h.UserGet(ctx, id)
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) Delete(ctx echo.Context, entityType string, id int) error {
switch entityType {
case "PasswordToken":
return h.PasswordTokenDelete(ctx, id)
case "User":
return h.UserDelete(ctx, id)
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) Update(ctx echo.Context, entityType string, id int) error {
switch entityType {
case "PasswordToken":
return h.PasswordTokenUpdate(ctx, id)
case "User":
return h.UserUpdate(ctx, id)
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) List(ctx echo.Context, entityType string) (*EntityList, error) {
switch entityType {
case "PasswordToken":
return h.PasswordTokenList(ctx)
case "User":
return h.UserList(ctx)
default:
return nil, fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) PasswordTokenCreate(ctx echo.Context) error {
var payload PasswordToken
if err := ctx.Bind(&payload); err != nil {
return err
}
op := h.client.PasswordToken.Create()
op.SetHash(payload.Hash)
op.SetCreatedAt(payload.CreatedAt)
op.SetUserID(payload.User)
_, err := op.Save(ctx.Request().Context())
return err
}
func (h *Handler) PasswordTokenUpdate(ctx echo.Context, id int) error {
entity, err := h.client.PasswordToken.Get(ctx.Request().Context(), id)
if err != nil {
return err
}
var payload PasswordToken
if err = ctx.Bind(&payload); err != nil {
return err
}
op := entity.Update()
op.SetHash(payload.Hash)
op.SetCreatedAt(payload.CreatedAt)
op.SetUserID(payload.User)
_, err = op.Save(ctx.Request().Context())
return err
}
func (h *Handler) PasswordTokenDelete(ctx echo.Context, id int) error {
return h.client.PasswordToken.DeleteOneID(id).
Exec(ctx.Request().Context())
}
func (h *Handler) PasswordTokenList(ctx echo.Context) (*EntityList, error) {
res, err := h.client.PasswordToken.
Query().
Limit(h.itemsPerPage + 1).
Offset(h.getOffset(ctx)).
Order(passwordtoken.ByID(sql.OrderDesc())).
All(ctx.Request().Context())
if err != nil {
return nil, err
}
list := &EntityList{
Columns: []string{
"Created at",
// "User", ?
},
Entities: make([]EntityValues, 0, len(res)),
HasNextPage: len(res) > h.itemsPerPage,
}
for i := 0; i < len(res)-1; i++ {
list.Entities = append(list.Entities, EntityValues{
ID: res[i].ID,
Values: []string{
fmt.Sprint(res[i].CreatedAt),
// TODO User ?
},
})
}
return list, err
}
func (h *Handler) PasswordTokenGet(ctx echo.Context, id int) error {
_, err := h.client.PasswordToken.Get(ctx.Request().Context(), id)
if err != nil {
return err
}
// TODO
return nil
}
func (h *Handler) UserCreate(ctx echo.Context) error {
var payload User
if err := ctx.Bind(&payload); err != nil {
return err
}
op := h.client.User.Create()
op.SetName(payload.Name)
op.SetEmail(payload.Email)
op.SetPassword(payload.Password)
op.SetVerified(payload.Verified)
op.SetCreatedAt(payload.CreatedAt)
_, err := op.Save(ctx.Request().Context())
return err
}
func (h *Handler) UserUpdate(ctx echo.Context, id int) error {
entity, err := h.client.User.Get(ctx.Request().Context(), id)
if err != nil {
return err
}
var payload User
if err = ctx.Bind(&payload); err != nil {
return err
}
op := entity.Update()
op.SetName(payload.Name)
op.SetEmail(payload.Email)
op.SetPassword(payload.Password)
op.SetVerified(payload.Verified)
_, err = op.Save(ctx.Request().Context())
return err
}
func (h *Handler) UserDelete(ctx echo.Context, id int) error {
return h.client.User.DeleteOneID(id).
Exec(ctx.Request().Context())
}
func (h *Handler) UserList(ctx echo.Context) (*EntityList, error) {
res, err := h.client.User.
Query().
Limit(h.itemsPerPage + 1).
Offset(h.getOffset(ctx)).
Order(user.ByID(sql.OrderDesc())).
All(ctx.Request().Context())
if err != nil {
return nil, err
}
list := &EntityList{
Columns: []string{
"Name",
"Email",
"Verified",
"Created at",
},
Entities: make([]EntityValues, 0, len(res)),
HasNextPage: len(res) > h.itemsPerPage,
}
for i := 0; i <= len(res)-1; i++ {
list.Entities = append(list.Entities, EntityValues{
ID: res[i].ID,
Values: []string{
res[i].Name,
res[i].Email,
fmt.Sprint(res[i].Verified),
fmt.Sprint(res[i].CreatedAt),
},
})
}
return list, err
}
func (h *Handler) UserGet(ctx echo.Context, id int) error {
_, err := h.client.User.Get(ctx.Request().Context(), id)
if err != nil {
return err
}
// TODO
return nil
}
func (h *Handler) getOffset(ctx echo.Context) int {
if page, err := strconv.Atoi(ctx.QueryParam("page")); err == nil {
if page > 1 {
return (page - 1) * h.itemsPerPage
}
}
return 0
}

View file

@ -0,0 +1,215 @@
{{/* Tell Intellij/GoLand to enable the autocompletion based on the *gen.Graph type. */}}
{{/* gotype: entgo.io/ent/entc/gen.Graph */}}
{{ define "admin/handler" }}
// Code generated by ent, DO NOT EDIT.
{{- $pkg := base $.Config.Package }}
package admin
import (
"fmt"
"strconv"
"entgo.io/ent/dialect/sql"
"github.com/labstack/echo/v4"
"{{ $.Config.Package }}"
{{- range $n := $.Nodes }}
"{{ $.Config.Package }}/{{ $n.Package }}"
{{- end }}
)
type Handler struct {
client *{{ $pkg }}.Client
itemsPerPage int
}
func NewHandler(client *{{ $pkg }}.Client, itemsPerPage int) *Handler {
return &Handler{
client: client,
itemsPerPage: itemsPerPage,
}
}
func (h *Handler) Create(ctx echo.Context, entityType string) error {
switch entityType {
{{- range $n := $.Nodes }}
case "{{ $n.Name }}":
return h.{{ $n.Name }}Create(ctx)
{{- end }}
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) Get(ctx echo.Context, entityType string, id int) error {
// TODO
switch entityType {
{{- range $n := $.Nodes }}
case "{{ $n.Name }}":
return h.{{ $n.Name }}Get(ctx, id)
{{- end }}
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) Delete(ctx echo.Context, entityType string, id int) error {
switch entityType {
{{- range $n := $.Nodes }}
case "{{ $n.Name }}":
return h.{{ $n.Name }}Delete(ctx, id)
{{- end }}
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) Update(ctx echo.Context, entityType string, id int) error {
switch entityType {
{{- range $n := $.Nodes }}
case "{{ $n.Name }}":
return h.{{ $n.Name }}Update(ctx, id)
{{- end }}
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) List(ctx echo.Context, entityType string) (*EntityList, error) {
switch entityType {
{{- range $n := $.Nodes }}
case "{{ $n.Name }}":
return h.{{ $n.Name }}List(ctx)
{{- end }}
default:
return nil, fmt.Errorf("unsupported entity type: %s", entityType)
}
}
{{ range $n := $.Nodes }}
func (h *Handler) {{ $n.Name }}Create(ctx echo.Context) error {
var payload {{ $n.Name }}
if err := ctx.Bind(&payload); err != nil {
return err
}
op := h.client.{{ $n.Name }}.Create()
{{- range $f := $n.Fields }}
op.Set{{ fieldName $f.Name }}(payload.{{ fieldName $f.Name }})
{{- end }}
{{- range $e := $n.Edges }}
{{- if not $e.Inverse}}
op.Set{{ fieldName $e.Name }}ID(payload.{{ fieldName $e.Name }})
{{- end }}
{{- end }}
_, err := op.Save(ctx.Request().Context())
return err
}
func (h *Handler) {{ $n.Name }}Update(ctx echo.Context, id int) error {
entity, err := h.client.{{ $n.Name }}.Get(ctx.Request().Context(), id)
if err != nil {
return err
}
var payload {{ $n.Name }}
if err = ctx.Bind(&payload); err != nil {
return err
}
op := entity.Update()
{{- range $f := $n.Fields }}
{{- if not $f.Immutable }}
op.Set{{ fieldName $f.Name }}(payload.{{ fieldName $f.Name }})
{{- end }}
{{- end }}
{{- range $e := $n.Edges }}
{{- if not $e.Inverse}}
op.Set{{ fieldName $e.Name }}ID(payload.{{ fieldName $e.Name }})
{{- end }}
{{- end }}
_, err = op.Save(ctx.Request().Context())
return err
}
func (h *Handler) {{ $n.Name }}Delete(ctx echo.Context, id int) error {
return h.client.{{ $n.Name }}.DeleteOneID(id).
Exec(ctx.Request().Context())
}
func (h *Handler) {{ $n.Name }}List(ctx echo.Context) (*EntityList, error) {
res, err := h.client.{{ $n.Name }}.
Query().
Limit(h.itemsPerPage+1).
Offset(h.getOffset(ctx)).
Order({{ $n.Package }}.ByID(sql.OrderDesc())).
All(ctx.Request().Context())
if err != nil {
return nil, err
}
list := &EntityList{
Columns: []string{
{{- range $f := $n.Fields }}
{{- if not $f.Sensitive }}
"{{ fieldLabel $f.Name }}",
{{- end }}
{{- end }}
{{- range $e := $n.Edges }}
{{- if not $e.Inverse}}
// "{{ fieldLabel $e.Name }}", ?
{{- end }}
{{- end }}
},
Entities: make([]EntityValues, 0, len(res)),
HasNextPage: len(res) > h.itemsPerPage,
}
for i := 0; i <= len(res)-1; i++ {
list.Entities = append(list.Entities, EntityValues{
ID: res[i].ID,
Values: []string{
{{- range $f := $n.Fields }}
{{- if not $f.Sensitive }}
{{- if eq $f.Type.String "string" }}
res[i].{{ fieldName $f.Name }},
{{- else }}
fmt.Sprint(res[i].{{ fieldName $f.Name }}),
{{- end }}
{{- end }}
{{- end }}
{{- range $e := $n.Edges }}
{{- if not $e.Inverse}}
// TODO {{ fieldName $e.Name }} ?
{{- end }}
{{- end }}
},
})
}
return list, err
}
func (h *Handler) {{ $n.Name }}Get(ctx echo.Context, id int) error {
_, err := h.client.{{ $n.Name }}.Get(ctx.Request().Context(), id)
if err != nil {
return err
}
// TODO
return nil
}
{{ end }}
func (h *Handler) getOffset(ctx echo.Context) int {
if page, err := strconv.Atoi(ctx.QueryParam("page")); err == nil {
if page > 1 {
return (page-1) * h.itemsPerPage
}
}
return 0
}
{{ end }}

View file

@ -0,0 +1,34 @@
{{/* Tell Intellij/GoLand to enable the autocompletion based on the *gen.Graph type. */}}
{{/* gotype: entgo.io/ent/entc/gen.Graph */}}
{{ define "admin/types" }}
// Code generated by ent, DO NOT EDIT.
package admin
{{ range $n := $.Nodes }}
type {{ $n.Name }} struct {
// Fields.
{{- range $f := $n.Fields }}
{{ fieldName $f.Name }} {{ $f.Type }} `form:"{{ $f.Name }}"`
{{- end }}
// Edges.
{{- range $e := $n.Edges }}
{{- if not $e.Inverse}}
{{ fieldName $e.Name }} int `form:"{{ $e.Name }}"`
{{- end }}
{{- end }}
}
{{ end }}
type EntityList struct {
Columns []string
Entities []EntityValues
HasNextPage bool
}
type EntityValues struct {
ID int
Values []string
}
{{ end }}

33
ent/admin/types.go Normal file
View file

@ -0,0 +1,33 @@
// Code generated by ent, DO NOT EDIT.
package admin
import "time"
type PasswordToken struct {
// Fields.
Hash string `form:"hash"`
CreatedAt time.Time `form:"created_at"`
// Edges.
User int `form:"user"`
}
type User struct {
// Fields.
Name string `form:"name"`
Email string `form:"email"`
Password string `form:"password"`
Verified bool `form:"verified"`
CreatedAt time.Time `form:"created_at"`
// Edges.
}
type EntityList struct {
Columns []string
Entities []EntityValues
HasNextPage bool
}
type EntityValues struct {
ID int
Values []string
}