- {{.Title}}
-
- {{.Body}}
-
-```
-
-Where `9fhe73kaf3` is the randomly-generated cache-buster.
-
-## Email
-
-An email client was added as a _Service_ to the `Container` but it is just a skeleton without any actual email-sending functionality. The reason is because there are a lot of ways to send email and most prefer using a SaaS solution for that. That makes it difficult to provide a generic solution that will work for most applications.
-
-Two starter methods were added to the `MailClient`, one to send an email via plain-text and one to send via a template by leveraging the [template renderer](#template-renderer). The standard library can be used if you wish to send email via SMTP and most SaaS providers have a Go package that can be used if you choose to go that direction.
-
-## HTTPS
-
-By default, the application will not use HTTPS but it can be enabled easily. Just alter the following configuration:
-
-- `Config.HTTP.TLS.Enabled`: `true`
-- `Config.HTTP.TLS.Certificate`: Full path to the certificate file
-- `Config.HTTP.TLS.Key`: Full path to the key file
-
-To use _Let's Encrypt_ follow [this guide](https://echo.labstack.com/cookbook/auto-tls/#server).
-
-## Logging
-
-Logging is provided by [Echo](https://echo.labstack.com/guide/customization/#logging) and is accessible within the _Echo_ instance, which is located in the `Web` field of the `Container`, or within any of the _context_ parameters, for example:
-
-```go
-func (c *Home) Get(ctx echo.Context) error {
- ctx.Logger().Info("something happened")
-
- if err := someOperation(); err != nil {
- ctx.Logger().Errorf("the operation failed: %v", err)
- }
-}
-```
-
-The logger can be swapped out for another, as long as it implements Echo's logging [interface](https://github.com/labstack/echo/blob/master/log.go). There are projects that provide this bridge for popular logging packages such as [zerolog](https://github.com/rs/zerolog).
-
-### Request ID
-
-By default, Echo's [request ID middleware](https://echo.labstack.com/middleware/request-id/) is enabled on the router but it only adds a request ID to the log entry for the HTTP request itself. Log entries that are created during the course of that request do not contain the request ID. `LogRequestID()` is custom middleware included which adds that request ID to all logs created throughout the request.
-
-## Roadmap
-
-Future work includes but is not limited to:
-
-- Email verification
-- Flexible pager templates
-- Expanded HTMX examples
-- Admin section
-
-## Credits
-
-Thank you to all of the following amazing projects for making this possible.
-
-- [go](https://go.dev/)
-- [echo](https://github.com/labstack/echo)
-- [ent](https://github.com/ent/ent)
-- [sprig](https://github.com/Masterminds/sprig)
-- [goquery](https://github.com/PuerkitoBio/goquery)
-- [validator](https://github.com/go-playground/validator)
-- [go-redis](https://github.com/go-redis/redis)
-- [gocache](https://github.com/eko/gocache)
-- [sessions](https://github.com/gorilla/sessions)
-- [pgx](https://github.com/jackc/pgx)
-- [envdecode](https://github.com/joeshaw/envdecode)
-- [testify](https://github.com/stretchr/testify)
-- [htmx](https://github.com/bigskysoftware/htmx)
-- [alpinejs](https://github.com/alpinejs/alpine)
-- [bulma](https://github.com/jgthms/bulma)
-- [docker](https://www.docker.com/)
-- [postgresql](https://www.postgresql.org/)
-- [redis](https://redis.io/)
\ No newline at end of file
+MIT — see LICENSE. Original Pagoda code Copyright (c) 2021 Mike Stefanello.
diff --git a/cmd/admin/main.go b/cmd/admin/main.go
new file mode 100644
index 0000000..046a587
--- /dev/null
+++ b/cmd/admin/main.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+
+ "github.com/camzawacki/personal-site/pkg/log"
+ "github.com/camzawacki/personal-site/pkg/services"
+)
+
+// main creates a new admin user with the email passed in via the flag.
+func main() {
+ // Start a new container.
+ c := services.NewContainer()
+ defer func() {
+ // Gracefully shutdown all services.
+ if err := c.Shutdown(); err != nil {
+ log.Default().Error("shutdown failed", "error", err)
+ }
+ }()
+
+ var email string
+ flag.StringVar(&email, "email", "", "email address for the admin user")
+ flag.Parse()
+
+ if len(email) == 0 {
+ invalid("email is required")
+ }
+
+ // Generate a password.
+ pw, err := c.Auth.RandomToken(10)
+ if err != nil {
+ invalid("failed to generate a random password")
+ }
+
+ // Create the admin user.
+ err = c.ORM.User.
+ Create().
+ SetEmail(email).
+ SetName("Admin").
+ SetAdmin(true).
+ SetVerified(true).
+ SetPassword(pw).
+ Exec(context.Background())
+
+ if err != nil {
+ invalid(err.Error())
+ }
+
+ fmt.Println("")
+ fmt.Println("-- ADMIN USER CREATED --")
+ fmt.Printf("Email: %s\n", email)
+ fmt.Printf("Password: %s\n", pw)
+ fmt.Println("----")
+ fmt.Println("")
+}
+
+func invalid(msg string) {
+ fmt.Printf("[ERROR] %s\n", msg)
+ os.Exit(1)
+}
diff --git a/cmd/web/main.go b/cmd/web/main.go
new file mode 100644
index 0000000..7d143d7
--- /dev/null
+++ b/cmd/web/main.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "context"
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "os/signal"
+
+ "github.com/camzawacki/personal-site/pkg/handlers"
+ "github.com/camzawacki/personal-site/pkg/log"
+ "github.com/camzawacki/personal-site/pkg/services"
+ "github.com/camzawacki/personal-site/pkg/tasks"
+)
+
+func main() {
+ // Start a new container.
+ c := services.NewContainer()
+ defer func() {
+ // Gracefully shutdown all services.
+ fatal("shutdown failed", c.Shutdown())
+ }()
+
+ // Build the router.
+ if err := handlers.BuildRouter(c); err != nil {
+ fatal("failed to build the router", err)
+ }
+
+ // Register all task queues.
+ tasks.Register(c)
+
+ // Start the task runner to execute queued tasks.
+ c.Tasks.Start(context.Background())
+
+ // Start the server.
+ go func() {
+ srv := http.Server{
+ Addr: fmt.Sprintf("%s:%d", c.Config.HTTP.Hostname, c.Config.HTTP.Port),
+ Handler: c.Web,
+ ReadTimeout: c.Config.HTTP.ReadTimeout,
+ WriteTimeout: c.Config.HTTP.WriteTimeout,
+ IdleTimeout: c.Config.HTTP.IdleTimeout,
+ }
+
+ if c.Config.HTTP.TLS.Enabled {
+ certs, err := tls.LoadX509KeyPair(c.Config.HTTP.TLS.Certificate, c.Config.HTTP.TLS.Key)
+ fatal("cannot load TLS certificate", err)
+
+ srv.TLSConfig = &tls.Config{
+ Certificates: []tls.Certificate{certs},
+ }
+ }
+
+ if err := c.Web.StartServer(&srv); errors.Is(err, http.ErrServerClosed) {
+ fatal("shutting down the server", err)
+ }
+ }()
+
+ // Wait for interrupt signal to gracefully shut down the web server and task runner.
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, os.Interrupt)
+ signal.Notify(quit, os.Kill)
+ <-quit
+}
+
+// fatal logs an error and terminates the application, if the error is not nil.
+func fatal(msg string, err error) {
+ if err != nil {
+ log.Default().Error(msg, "error", err)
+ os.Exit(1)
+ }
+}
diff --git a/config/config.go b/config/config.go
index 3077721..c74607d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -2,115 +2,146 @@ package config
import (
"os"
+ "strings"
"time"
- "github.com/joeshaw/envdecode"
+ "github.com/spf13/viper"
)
-const (
- // TemplateDir stores the name of the directory that contains templates
- TemplateDir = "templates"
-
- // TemplateExt stores the extension used for the template files
- TemplateExt = ".gohtml"
-
- // StaticDir stores the name of the directory that will serve static files
- StaticDir = "static"
-
- // StaticPrefix stores the URL prefix used when serving static files
- StaticPrefix = "files"
-)
-
-type Environment string
+type environment string
const (
- EnvLocal Environment = "local"
- EnvTest Environment = "test"
- EnvDevelop Environment = "dev"
- EnvStaging Environment = "staging"
- EnvQA Environment = "qa"
- EnvProduction Environment = "prod"
+ // EnvLocal represents the local environment.
+ EnvLocal environment = "local"
+
+ // EnvTest represents the test environment.
+ EnvTest environment = "test"
+
+ // EnvDevelopment represents the development environment.
+ EnvDevelopment environment = "dev"
+
+ // EnvStaging represents the staging environment.
+ EnvStaging environment = "staging"
+
+ // EnvQA represents the qa environment.
+ EnvQA environment = "qa"
+
+ // EnvProduction represents the production environment.
+ EnvProduction environment = "prod"
)
// SwitchEnvironment sets the environment variable used to dictate which environment the application is
// currently running in.
// This must be called prior to loading the configuration in order for it to take effect.
-func SwitchEnvironment(env Environment) {
- if err := os.Setenv("APP_ENVIRONMENT", string(env)); err != nil {
+func SwitchEnvironment(env environment) {
+ if err := os.Setenv("PAGODA_APP_ENVIRONMENT", string(env)); err != nil {
panic(err)
}
}
type (
- // Config stores complete configuration
+ // Config stores complete configuration.
Config struct {
HTTP HTTPConfig
App AppConfig
Cache CacheConfig
Database DatabaseConfig
+ Files FilesConfig
+ Tasks TasksConfig
Mail MailConfig
}
- // HTTPConfig stores HTTP configuration
+ // HTTPConfig stores HTTP configuration.
HTTPConfig struct {
- Hostname string `env:"HTTP_HOSTNAME"`
- Port uint16 `env:"HTTP_PORT,default=8000"`
- ReadTimeout time.Duration `env:"HTTP_READ_TIMEOUT,default=5s"`
- WriteTimeout time.Duration `env:"HTTP_WRITE_TIMEOUT,default=10s"`
- IdleTimeout time.Duration `env:"HTTP_IDLE_TIMEOUT,default=2m"`
- TLS struct {
- Enabled bool `env:"HTTP_TLS_ENABLED,default=false"`
- Certificate string `env:"HTTP_TLS_CERTIFICATE"`
- Key string `env:"HTTP_TLS_KEY"`
+ Hostname string
+ Port uint16
+ ReadTimeout time.Duration
+ WriteTimeout time.Duration
+ IdleTimeout time.Duration
+ ShutdownTimeout time.Duration
+ TLS struct {
+ Enabled bool
+ Certificate string
+ Key string
}
}
- // AppConfig stores application configuration
+ // AppConfig stores application configuration.
AppConfig struct {
- Name string `env:"APP_NAME,default=Goweb"`
- Environment Environment `env:"APP_ENVIRONMENT,default=local"`
- EncryptionKey string `env:"APP_ENCRYPTION_KEY,default=?E(G+KbPeShVmYq3t6w9z$C&F)J@McQf"`
- Timeout time.Duration `env:"APP_TIMEOUT,default=20s"`
+ Name string
+ Host string
+ Environment environment
+ EncryptionKey string
+ Timeout time.Duration
PasswordToken struct {
- Expiration time.Duration `env:"APP_PASSWORD_TOKEN_EXPIRATION,default=60m"`
- Length int `env:"APP_PASSWORD_TOKEN_LENGTH,default=64"`
+ Expiration time.Duration
+ Length int
}
+ EmailVerificationTokenExpiration time.Duration
}
- // CacheConfig stores the cache configuration
+ // CacheConfig stores the cache configuration.
CacheConfig struct {
- Hostname string `env:"CACHE_HOSTNAME,default=localhost"`
- Port uint16 `env:"CACHE_PORT,default=6379"`
- Password string `env:"CACHE_PASSWORD"`
+ Capacity int
Expiration struct {
- StaticFile time.Duration `env:"CACHE_EXPIRATION_STATIC_FILE,default=4380h"`
- Page time.Duration `env:"CACHE_EXPIRATION_PAGE,default=24h"`
+ PublicFile time.Duration
}
}
- // DatabaseConfig stores the database configuration
+ // DatabaseConfig stores the database configuration.
DatabaseConfig struct {
- Hostname string `env:"DB_HOSTNAME,default=localhost"`
- Port uint16 `env:"DB_PORT,default=5432"`
- User string `env:"DB_USER,default=admin"`
- Password string `env:"DB_PASSWORD,default=admin"`
- Database string `env:"DB_NAME,default=app"`
- TestDatabase string `env:"DB_NAME_TEST,default=app_test"`
+ Driver string
+ Connection string
+ TestConnection string
}
- // MailConfig stores the mail configuration
+ // FilesConfig stores the file system configuration.
+ FilesConfig struct {
+ Directory string
+ }
+
+ // TasksConfig stores the tasks configuration.
+ TasksConfig struct {
+ Goroutines int
+ ReleaseAfter time.Duration
+ CleanupInterval time.Duration
+ ShutdownTimeout time.Duration
+ }
+
+ // MailConfig stores the mail configuration.
MailConfig struct {
- Hostname string `env:"MAIL_HOSTNAME,default=localhost"`
- Port uint16 `env:"MAIL_PORT,default=25"`
- User string `env:"MAIL_USER,default=admin"`
- Password string `env:"MAIL_PASSWORD,default=admin"`
- FromAddress string `env:"MAIL_FROM_ADDRESS,default=admin@localhost"`
+ Hostname string
+ Port uint16
+ User string
+ Password string
+ FromAddress string
}
)
-// GetConfig loads and returns configuration
+// GetConfig loads and returns configuration.
func GetConfig() (Config, error) {
- var cfg Config
- err := envdecode.StrictDecode(&cfg)
- return cfg, err
+ var c Config
+
+ // Load the config file.
+ viper.SetConfigName("config")
+ viper.SetConfigType("yaml")
+ viper.AddConfigPath(".")
+ viper.AddConfigPath("config")
+ viper.AddConfigPath("../config")
+ viper.AddConfigPath("../../config")
+
+ // Load env variables.
+ viper.SetEnvPrefix("pagoda")
+ viper.AutomaticEnv()
+ viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+
+ if err := viper.ReadInConfig(); err != nil {
+ return c, err
+ }
+
+ if err := viper.Unmarshal(&c); err != nil {
+ return c, err
+ }
+
+ return c, nil
}
diff --git a/config/config.yaml b/config/config.yaml
new file mode 100644
index 0000000..06ce5e4
--- /dev/null
+++ b/config/config.yaml
@@ -0,0 +1,54 @@
+http:
+ hostname: ""
+ port: 8000
+ readTimeout: "5s"
+ writeTimeout: "10s"
+ idleTimeout: "2m"
+ shutdownTimeout: "10s"
+ tls:
+ enabled: false
+ certificate: ""
+ key: ""
+
+app:
+ name: "Pagoda"
+ # We manually set this rather than using the HTTP settings in order to build absolute URLs for users
+ # since it's likely your app's HTTP settings are not identical to what is exposed by your server.
+ host: "http://localhost:8000"
+ environment: "local"
+ # Change this on any live environments.
+ encryptionKey: "?E(G+KbPeShVmYq3t6w9z$C&F)J@McQf"
+ timeout: "20s"
+ passwordToken:
+ expiration: "60m"
+ length: 64
+ emailVerificationTokenExpiration: "12h"
+
+cache:
+ capacity: 100000
+ expiration:
+ publicFile: "4380h"
+
+database:
+ driver: "sqlite3"
+ connection: "dbs/main.db?_journal=WAL&_timeout=5000&_fk=true"
+ # $RAND will be automatically replaced with a random value.
+ # memdb is more robust for an in-memory database rather than :memory: because the latter has the potential
+ # retain data even after you close and re-open the connection.
+ testConnection: "file:/$RAND?vfs=memdb&_timeout=1000&_fk=true"
+
+files:
+ directory: "uploads"
+
+tasks:
+ goroutines: 1
+ releaseAfter: "15m"
+ cleanupInterval: "1h"
+ shutdownTimeout: "10s"
+
+mail:
+ hostname: "localhost"
+ port: 25
+ user: "admin"
+ password: "admin"
+ fromAddress: "admin@localhost"
diff --git a/config/config_test.go b/config/config_test.go
index 103e368..1404f7c 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -11,7 +11,7 @@ func TestGetConfig(t *testing.T) {
_, err := GetConfig()
require.NoError(t, err)
- var env Environment
+ var env environment
env = "abc"
SwitchEnvironment(env)
cfg, err := GetConfig()
diff --git a/context/context.go b/context/context.go
deleted file mode 100644
index 177bf30..0000000
--- a/context/context.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package context
-
-const (
- // AuthenticatedUserKey is the key value used to store the authenticated user in context
- AuthenticatedUserKey = "auth_user"
-
- // UserKey is the key value used to store a user in context
- UserKey = "user"
-
- // FormKey is the key value used to store a form in context
- FormKey = "form"
-
- // PasswordTokenKey is the key value used to store a password token in context
- PasswordTokenKey = "password_token"
-)
diff --git a/controller/controller.go b/controller/controller.go
deleted file mode 100644
index 7579d7b..0000000
--- a/controller/controller.go
+++ /dev/null
@@ -1,163 +0,0 @@
-package controller
-
-import (
- "bytes"
- "fmt"
- "net/http"
-
- "goweb/middleware"
- "goweb/services"
-
- "github.com/eko/gocache/v2/marshaler"
-
- "github.com/eko/gocache/v2/store"
-
- "github.com/labstack/echo/v4"
-)
-
-// Controller provides base functionality and dependencies to routes.
-// The proposed pattern is to embed a Controller in each individual route struct and to use
-// the router to inject the container so your routes have access to the services within the container
-type Controller struct {
- // Container stores a services container which contains dependencies
- Container *services.Container
-}
-
-// NewController creates a new Controller
-func NewController(c *services.Container) Controller {
- return Controller{
- Container: c,
- }
-}
-
-// RenderPage renders a Page as an HTTP response
-func (c *Controller) RenderPage(ctx echo.Context, page Page) error {
- var buf *bytes.Buffer
- var err error
-
- // Page name is required
- if page.Name == "" {
- ctx.Logger().Error("page render failed due to missing name")
- return echo.NewHTTPError(http.StatusInternalServerError)
- }
-
- // Use the app name in configuration if a value was not set
- if page.AppName == "" {
- page.AppName = c.Container.Config.App.Name
- }
-
- // Check if this is an HTMX non-boosted request which indicates that only partial
- // content should be rendered
- if page.HTMX.Request.Enabled && !page.HTMX.Request.Boosted {
- // Parse and execute the templates only for the content portion of the page
- // The templates used for this partial request will be:
- // 1. The base htmx template which omits the layout and only includes the content template
- // 2. The content template specified in Page.Name
- // 3. All templates within the components directory
- // Also included is the function map provided by the funcmap package
- buf, err = c.Container.TemplateRenderer.ParseAndExecute(
- "page:htmx",
- page.Name,
- "htmx",
- []string{
- "htmx",
- fmt.Sprintf("pages/%s", page.Name),
- },
- []string{"components"},
- page,
- )
- } else {
- // Parse and execute the templates for the Page
- // As mentioned in the documentation for the Page struct, the templates used for the page will be:
- // 1. The layout/base template specified in Page.Layout
- // 2. The content template specified in Page.Name
- // 3. All templates within the components directory
- // Also included is the function map provided by the funcmap package
- buf, err = c.Container.TemplateRenderer.ParseAndExecute(
- "page",
- page.Name,
- page.Layout,
- []string{
- fmt.Sprintf("layouts/%s", page.Layout),
- fmt.Sprintf("pages/%s", page.Name),
- },
- []string{"components"},
- page,
- )
- }
-
- if err != nil {
- ctx.Logger().Errorf("failed to parse and execute templates: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError)
- }
-
- // Set the status code
- ctx.Response().Status = page.StatusCode
-
- // Set any headers
- for k, v := range page.Headers {
- ctx.Response().Header().Set(k, v)
- }
-
- // Apply the HTMX response, if one
- if page.HTMX.Response != nil {
- page.HTMX.Response.Apply(ctx)
- }
-
- // Cache this page, if caching was enabled
- c.cachePage(ctx, page, buf)
-
- return ctx.HTMLBlob(ctx.Response().Status, buf.Bytes())
-}
-
-// cachePage caches the HTML for a given Page if the Page has caching enabled
-func (c *Controller) cachePage(ctx echo.Context, page Page, html *bytes.Buffer) {
- if !page.Cache.Enabled {
- return
- }
-
- // If no expiration time was provided, default to the configuration value
- if page.Cache.Expiration == 0 {
- page.Cache.Expiration = c.Container.Config.Cache.Expiration.Page
- }
-
- // Extract the headers
- headers := make(map[string]string)
- for k, v := range ctx.Response().Header() {
- headers[k] = v[0]
- }
-
- // The request URL is used as the cache key so the middleware can serve the
- // cached page on matching requests
- key := ctx.Request().URL.String()
- opts := &store.Options{
- Expiration: page.Cache.Expiration,
- Tags: page.Cache.Tags,
- }
- cp := middleware.CachedPage{
- URL: key,
- HTML: html.Bytes(),
- Headers: headers,
- StatusCode: ctx.Response().Status,
- }
-
- err := marshaler.New(c.Container.Cache).Set(ctx.Request().Context(), key, cp, opts)
- if err != nil {
- ctx.Logger().Errorf("failed to cache page: %v", err)
- return
- }
-
- ctx.Logger().Infof("cached page")
-}
-
-// Redirect redirects to a given route name with optional route parameters
-func (c *Controller) Redirect(ctx echo.Context, route string, routeParams ...interface{}) error {
- url := ctx.Echo().Reverse(route, routeParams)
- return ctx.Redirect(http.StatusFound, url)
-}
-
-// Fail is a helper to fail a request by returning a 500 error and logging the error
-func (c *Controller) Fail(ctx echo.Context, err error, log string) error {
- ctx.Logger().Errorf("%s: %v", log, err)
- return echo.NewHTTPError(http.StatusInternalServerError)
-}
diff --git a/controller/controller_test.go b/controller/controller_test.go
deleted file mode 100644
index 3c85b19..0000000
--- a/controller/controller_test.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package controller
-
-import (
- "context"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "os"
- "testing"
-
- "goweb/config"
- "goweb/htmx"
- "goweb/middleware"
- "goweb/services"
- "goweb/tests"
-
- "github.com/eko/gocache/v2/store"
-
- "github.com/eko/gocache/v2/marshaler"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/labstack/echo/v4"
-)
-
-var (
- c *services.Container
-)
-
-func TestMain(m *testing.M) {
- // Set the environment to test
- config.SwitchEnvironment(config.EnvTest)
-
- // Create a new container
- c = services.NewContainer()
- defer func() {
- if err := c.Shutdown(); err != nil {
- c.Web.Logger.Fatal(err)
- }
- }()
-
- // Run tests
- exitVal := m.Run()
- os.Exit(exitVal)
-}
-
-func TestController_Redirect(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/abc")
- ctr := NewController(c)
- err := ctr.Redirect(ctx, "home")
- require.NoError(t, err)
- assert.Equal(t, "", ctx.Response().Header().Get(echo.HeaderLocation))
- assert.Equal(t, http.StatusFound, ctx.Response().Status)
-}
-
-func TestController_RenderPage(t *testing.T) {
- setup := func() (echo.Context, *httptest.ResponseRecorder, Controller, Page) {
- ctx, rec := tests.NewContext(c.Web, "/test/TestController_RenderPage")
- tests.InitSession(ctx)
- ctr := NewController(c)
-
- p := NewPage(ctx)
- p.Name = "home"
- p.Layout = "main"
- p.Cache.Enabled = false
- p.Headers["A"] = "b"
- p.Headers["C"] = "d"
- p.StatusCode = http.StatusCreated
- return ctx, rec, ctr, p
- }
-
- t.Run("missing name", func(t *testing.T) {
- // Rendering should fail if the Page has no name
- ctx, _, ctr, p := setup()
- p.Name = ""
- err := ctr.RenderPage(ctx, p)
- assert.Error(t, err)
- })
-
- t.Run("no page cache", func(t *testing.T) {
- ctx, _, ctr, p := setup()
- err := ctr.RenderPage(ctx, p)
- require.NoError(t, err)
-
- // Check status code and headers
- assert.Equal(t, http.StatusCreated, ctx.Response().Status)
- for k, v := range p.Headers {
- assert.Equal(t, v, ctx.Response().Header().Get(k))
- }
-
- // Check the template cache
- parsed, err := c.TemplateRenderer.Load("page", p.Name)
- assert.NoError(t, err)
-
- // Check that all expected templates were parsed.
- // This includes the name, layout and all components
- expectedTemplates := make(map[string]bool)
- expectedTemplates[p.Name+config.TemplateExt] = true
- expectedTemplates[p.Layout+config.TemplateExt] = true
- components, err := ioutil.ReadDir(c.TemplateRenderer.GetTemplatesPath() + "/components")
- require.NoError(t, err)
- for _, f := range components {
- expectedTemplates[f.Name()] = true
- }
-
- for _, v := range parsed.Templates() {
- delete(expectedTemplates, v.Name())
- }
- assert.Empty(t, expectedTemplates)
- })
-
- t.Run("htmx rendering", func(t *testing.T) {
- ctx, _, ctr, p := setup()
- p.HTMX.Request.Enabled = true
- p.HTMX.Response = &htmx.Response{
- Trigger: "trigger",
- }
- err := ctr.RenderPage(ctx, p)
- require.NoError(t, err)
-
- // Check HTMX header
- assert.Equal(t, "trigger", ctx.Response().Header().Get(htmx.HeaderTrigger))
-
- // Check the template cache
- parsed, err := c.TemplateRenderer.Load("page:htmx", p.Name)
- assert.NoError(t, err)
-
- // Check that all expected templates were parsed.
- // This includes the name, htmx and all components
- expectedTemplates := make(map[string]bool)
- expectedTemplates[p.Name+config.TemplateExt] = true
- expectedTemplates["htmx"+config.TemplateExt] = true
- components, err := ioutil.ReadDir(c.TemplateRenderer.GetTemplatesPath() + "/components")
- require.NoError(t, err)
- for _, f := range components {
- expectedTemplates[f.Name()] = true
- }
-
- for _, v := range parsed.Templates() {
- delete(expectedTemplates, v.Name())
- }
- assert.Empty(t, expectedTemplates)
- })
-
- t.Run("page cache", func(t *testing.T) {
- ctx, rec, ctr, p := setup()
- p.Cache.Enabled = true
- p.Cache.Tags = []string{"tag1"}
- err := ctr.RenderPage(ctx, p)
- require.NoError(t, err)
-
- // Fetch from the cache
- res, err := marshaler.New(c.Cache).
- Get(context.Background(), p.URL, new(middleware.CachedPage))
- require.NoError(t, err)
-
- // Compare the cached page
- cp, ok := res.(*middleware.CachedPage)
- require.True(t, ok)
- assert.Equal(t, p.URL, cp.URL)
- assert.Equal(t, p.Headers, cp.Headers)
- assert.Equal(t, p.StatusCode, cp.StatusCode)
- assert.Equal(t, rec.Body.Bytes(), cp.HTML)
-
- // Clear the tag
- err = c.Cache.Invalidate(context.Background(), store.InvalidateOptions{
- Tags: []string{p.Cache.Tags[0]},
- })
- require.NoError(t, err)
-
- // Refetch from the cache and expect no results
- _, err = marshaler.New(c.Cache).
- Get(context.Background(), p.URL, new(middleware.CachedPage))
- assert.Error(t, err)
- })
-}
diff --git a/controller/form.go b/controller/form.go
deleted file mode 100644
index 78680e8..0000000
--- a/controller/form.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package controller
-
-import (
- "github.com/go-playground/validator/v10"
-
- "github.com/labstack/echo/v4"
-)
-
-// FormSubmission represents the state of the submission of a form, not including the form itself
-type FormSubmission struct {
- // IsSubmitted indicates if the form has been submitted
- IsSubmitted bool
-
- // Errors stores a slice of error message strings keyed by form struct field name
- Errors map[string][]string
-}
-
-// Process processes a submission for a form
-func (f *FormSubmission) Process(ctx echo.Context, form interface{}) error {
- f.Errors = make(map[string][]string)
- f.IsSubmitted = true
-
- // Validate the form
- if err := ctx.Validate(form); err != nil {
- f.setErrorMessages(err)
- }
-
- return nil
-}
-
-// HasErrors indicates if the submission has any validation errors
-func (f FormSubmission) HasErrors() bool {
- if f.Errors == nil {
- return false
- }
- return len(f.Errors) > 0
-}
-
-// FieldHasErrors indicates if a given field on the form has any validation errors
-func (f FormSubmission) FieldHasErrors(fieldName string) bool {
- return len(f.GetFieldErrors(fieldName)) > 0
-}
-
-// SetFieldError sets an error message for a given field name
-func (f *FormSubmission) SetFieldError(fieldName string, message string) {
- if f.Errors == nil {
- f.Errors = make(map[string][]string)
- }
- f.Errors[fieldName] = append(f.Errors[fieldName], message)
-}
-
-// GetFieldErrors gets the errors for a given field name
-func (f FormSubmission) GetFieldErrors(fieldName string) []string {
- if f.Errors == nil {
- return []string{}
- }
- return f.Errors[fieldName]
-}
-
-// GetFieldStatusClass returns an HTML class based on the status of the field
-func (f FormSubmission) GetFieldStatusClass(fieldName string) string {
- if f.IsSubmitted {
- if f.FieldHasErrors(fieldName) {
- return "is-danger"
- }
- return "is-success"
- }
- return ""
-}
-
-// IsDone indicates if the submission is considered done which is when it has been submitted
-// and there are no errors.
-func (f FormSubmission) IsDone() bool {
- return f.IsSubmitted && !f.HasErrors()
-}
-
-// setErrorMessages sets errors messages on the submission for all fields that failed validation
-func (f *FormSubmission) setErrorMessages(err error) {
- // Only this is supported right now
- ves, ok := err.(validator.ValidationErrors)
- if !ok {
- return
- }
-
- for _, ve := range ves {
- var message string
-
- // Provide better error messages depending on the failed validation tag
- // This should be expanded as you use additional tags in your validation
- switch ve.Tag() {
- case "required":
- message = "This field is required."
- case "email":
- message = "Enter a valid email address."
- case "eqfield":
- message = "Does not match."
- default:
- message = "Invalid value."
- }
-
- // Add the error
- f.SetFieldError(ve.Field(), message)
- }
-}
diff --git a/controller/form_test.go b/controller/form_test.go
deleted file mode 100644
index 1679dd4..0000000
--- a/controller/form_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package controller
-
-import (
- "testing"
-
- "goweb/tests"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestFormSubmission(t *testing.T) {
- type formTest struct {
- Name string `validate:"required"`
- Email string `validate:"required,email"`
- Submission FormSubmission
- }
-
- ctx, _ := tests.NewContext(c.Web, "/")
- form := formTest{
- Name: "",
- Email: "a@a.com",
- }
- err := form.Submission.Process(ctx, form)
- assert.NoError(t, err)
-
- assert.True(t, form.Submission.HasErrors())
- assert.True(t, form.Submission.FieldHasErrors("Name"))
- assert.False(t, form.Submission.FieldHasErrors("Email"))
- require.Len(t, form.Submission.GetFieldErrors("Name"), 1)
- assert.Len(t, form.Submission.GetFieldErrors("Email"), 0)
- assert.Equal(t, "This field is required.", form.Submission.GetFieldErrors("Name")[0])
- assert.Equal(t, "is-danger", form.Submission.GetFieldStatusClass("Name"))
- assert.Equal(t, "is-success", form.Submission.GetFieldStatusClass("Email"))
- assert.False(t, form.Submission.IsDone())
-}
diff --git a/controller/page.go b/controller/page.go
deleted file mode 100644
index 0922f71..0000000
--- a/controller/page.go
+++ /dev/null
@@ -1,161 +0,0 @@
-package controller
-
-import (
- "html/template"
- "net/http"
- "time"
-
- "goweb/context"
- "goweb/ent"
- "goweb/htmx"
- "goweb/msg"
-
- echomw "github.com/labstack/echo/v4/middleware"
-
- "github.com/labstack/echo/v4"
-)
-
-// Page consists of all data that will be used to render a page response for a given controller.
-// While it's not required for a controller to render a Page on a route, this is the common data
-// object that will be passed to the templates, making it easy for all controllers to share
-// functionality both on the back and frontend. The Page can be expanded to include anything else
-// your app wants to support.
-// Methods on this page also then become available in the templates, which can be more useful than
-// the funcmap if your methods require data stored in the page, such as the context.
-type Page struct {
- // AppName stores the name of the application.
- // If omitted, the configuration value will be used.
- AppName string
-
- // Title stores the title of the page
- Title string
-
- // Context stores the request context
- Context echo.Context
-
- // ToURL is a function to convert a route name and optional route parameters to a URL
- ToURL func(name string, params ...interface{}) string
-
- // Path stores the path of the current request
- Path string
-
- // URL stores the URL of the current request
- URL string
-
- // Data stores whatever additional data that needs to be passed to the templates.
- // This is what the controller uses to pass the content of the page.
- Data interface{}
-
- // Form stores a struct that represents a form on the page.
- // This should be a struct with fields for each form field, using both "form" and "validate" tags
- // It should also contain a Submission field of type FormSubmission if you wish to have validation
- // messagesa and markup presented to the user
- Form interface{}
-
- // Layout stores the name of the layout base template file which will be used when the page is rendered.
- // This should match a template file located within the layouts directory inside the templates directory.
- // The template extension should not be included in this value.
- Layout string
-
- // Name stores the name of the page as well as the name of the template file which will be used to render
- // the content portion of the layout template.
- // This should match a template file located within the pages directory inside the templates directory.
- // The template extension should not be included in this value.
- Name string
-
- // IsHome stores whether the requested page is the home page or not
- IsHome bool
-
- // IsAuth stores whether or not the user is authenticated
- IsAuth bool
-
- // AuthUser stores the authenticated user
- AuthUser *ent.User
-
- // StatusCode stores the HTTP status code that will be returned
- StatusCode int
-
- // Metatags stores metatag values
- Metatags struct {
- // Description stores the description metatag value
- Description string
-
- // Keywords stores the keywords metatag values
- Keywords []string
- }
-
- // Pager stores a pager which can be used to page lists of results
- Pager Pager
-
- // CSRF stores the CSRF token for the given request.
- // This will only be populated if the CSRF middleware is in effect for the given request.
- // If this is populated, all forms must include this value otherwise the requests will be rejected.
- CSRF string
-
- // Headers stores a list of HTTP headers and values to be set on the response
- Headers map[string]string
-
- // RequestID stores the ID of the given request.
- // This will only be populated if the request ID middleware is in effect for the given request.
- RequestID string
-
- HTMX struct {
- Request htmx.Request
- Response *htmx.Response
- }
-
- // Cache stores values for caching the response of this page
- Cache struct {
- // Enabled dictates if the response of this page should be cached.
- // Cached responses are served via middleware.
- Enabled bool
-
- // Expiration stores the amount of time that the cache entry should live for before expiring.
- // If omitted, the configuration value will be used.
- Expiration time.Duration
-
- // Tags stores a list of tags to apply to the cache entry.
- // These are useful when invalidating cache for dynamic events such as entity operations.
- Tags []string
- }
-}
-
-// NewPage creates and initiatizes a new Page for a given request context
-func NewPage(ctx echo.Context) Page {
- p := Page{
- Context: ctx,
- ToURL: ctx.Echo().Reverse,
- Path: ctx.Request().URL.Path,
- URL: ctx.Request().URL.String(),
- StatusCode: http.StatusOK,
- Pager: NewPager(ctx, DefaultItemsPerPage),
- Headers: make(map[string]string),
- RequestID: ctx.Response().Header().Get(echo.HeaderXRequestID),
- }
-
- p.IsHome = p.Path == "/"
-
- if csrf := ctx.Get(echomw.DefaultCSRFConfig.ContextKey); csrf != nil {
- p.CSRF = csrf.(string)
- }
-
- if u := ctx.Get(context.AuthenticatedUserKey); u != nil {
- p.IsAuth = true
- p.AuthUser = u.(*ent.User)
- }
-
- p.HTMX.Request = htmx.GetRequest(ctx)
-
- return p
-}
-
-// GetMessages gets all flash messages for a given type.
-// This allows for easy access to flash messages from the templates.
-func (p Page) GetMessages(typ msg.Type) []template.HTML {
- strs := msg.Get(p.Context, typ)
- ret := make([]template.HTML, len(strs))
- for k, v := range strs {
- ret[k] = template.HTML(v)
- }
- return ret
-}
diff --git a/controller/page_test.go b/controller/page_test.go
deleted file mode 100644
index b07620f..0000000
--- a/controller/page_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package controller
-
-import (
- "net/http"
- "testing"
-
- "goweb/context"
- "goweb/msg"
- "goweb/tests"
-
- echomw "github.com/labstack/echo/v4/middleware"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestNewPage(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/")
- p := NewPage(ctx)
- assert.Same(t, ctx, p.Context)
- assert.NotNil(t, p.ToURL)
- assert.Equal(t, "/", p.Path)
- assert.Equal(t, "/", p.URL)
- assert.Equal(t, http.StatusOK, p.StatusCode)
- assert.Equal(t, NewPager(ctx, DefaultItemsPerPage), p.Pager)
- assert.Empty(t, p.Headers)
- assert.True(t, p.IsHome)
- assert.False(t, p.IsAuth)
- assert.Empty(t, p.CSRF)
- assert.Empty(t, p.RequestID)
- assert.False(t, p.Cache.Enabled)
-
- ctx, _ = tests.NewContext(c.Web, "/abc?def=123")
- usr, err := tests.CreateUser(c.ORM)
- require.NoError(t, err)
- ctx.Set(context.AuthenticatedUserKey, usr)
- ctx.Set(echomw.DefaultCSRFConfig.ContextKey, "csrf")
- p = NewPage(ctx)
- assert.Equal(t, "/abc", p.Path)
- assert.Equal(t, "/abc?def=123", p.URL)
- assert.False(t, p.IsHome)
- assert.True(t, p.IsAuth)
- assert.Equal(t, usr, p.AuthUser)
- assert.Equal(t, "csrf", p.CSRF)
-}
-
-func TestPage_GetMessages(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/")
- tests.InitSession(ctx)
- p := NewPage(ctx)
-
- // Set messages
- msgTests := make(map[msg.Type][]string)
- msgTests[msg.TypeWarning] = []string{
- "abc",
- "def",
- }
- msgTests[msg.TypeInfo] = []string{
- "123",
- "456",
- }
- for typ, values := range msgTests {
- for _, value := range values {
- msg.Set(ctx, typ, value)
- }
- }
-
- // Get the messages
- for typ, values := range msgTests {
- msgs := p.GetMessages(typ)
-
- for i, message := range msgs {
- assert.Equal(t, values[i], string(message))
- }
- }
-}
diff --git a/docker-compose.yml b/docker-compose.yml
index ad78bd2..1c25846 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,15 +1,24 @@
-version: "3"
-
services:
- cache:
- image: "redis:alpine"
- ports:
- - "6379:6379"
- db:
- image: postgres:alpine
- ports:
- - "5432:5432"
+ app:
+ build: .
+ container_name: personal-site
+ restart: unless-stopped
environment:
- - POSTGRES_USER=admin
- - POSTGRES_PASSWORD=admin
- - POSTGRES_DB=app
\ No newline at end of file
+ - PAGODA_APP_ENVIRONMENT=production
+ - PAGODA_APP_HOST=camzalewski.com
+ - PAGODA_HTTP_HOSTNAME=0.0.0.0
+ - PAGODA_HTTP_PORT=8000
+ - PAGODA_APP_ENCRYPTIONKEY=${ENCRYPTION_KEY}
+ volumes:
+ - sqlite_data:/app/dbs
+ - uploads:/app/uploads
+ networks:
+ - web
+
+volumes:
+ sqlite_data:
+ uploads:
+
+networks:
+ web:
+ external: true
diff --git a/ent/admin/extension.go b/ent/admin/extension.go
new file mode 100644
index 0000000..308e2eb
--- /dev/null
+++ b/ent/admin/extension.go
@@ -0,0 +1,97 @@
+package admin
+
+import (
+ "embed"
+ "strings"
+ "text/template"
+ "unicode"
+
+ "entgo.io/ent/entc"
+ "entgo.io/ent/entc/gen"
+ "entgo.io/ent/schema/field"
+)
+
+var (
+ //go:embed templates
+ templateDir embed.FS
+)
+
+// Extension is the Ent extension that generates code to support the entity admin panel.
+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,
+ "fieldIsPointer": fieldIsPointer,
+ }).
+ ParseFS(templateDir, "templates/*tmpl"),
+ ),
+ }
+}
+
+// fieldName provides a struct field name from an entity field name (ie, user_id -> UserID).
+func fieldName(name string) string {
+ if len(name) == 0 {
+ return name
+ }
+
+ parts := strings.Split(name, "_")
+ for i := 0; i < len(parts); i++ {
+ if parts[i] == "id" {
+ parts[i] = "ID"
+ } else {
+ parts[i] = upperFirst(parts[i])
+ }
+ }
+
+ return strings.Join(parts, "")
+}
+
+// FieldLabel provides a label for an entity field name (ie, user_id -> User ID).
+func FieldLabel(name string) string {
+ if len(name) == 0 {
+ return name
+ }
+
+ parts := strings.Split(name, "_")
+ for i := 0; i < len(parts); i++ {
+ if parts[i] == "id" {
+ parts[i] = "ID"
+ }
+ if i == 0 {
+ parts[i] = upperFirst(parts[i])
+ }
+ }
+
+ return strings.Join(parts, " ")
+}
+
+// fieldIsPointer determines if a given entity field should be a pointer on the struct.
+func fieldIsPointer(f *gen.Field) bool {
+ switch {
+ case f.Type.Type == field.TypeBool:
+ return false
+ case f.Optional,
+ f.Default,
+ f.Sensitive(),
+ f.Nillable:
+ return true
+ }
+ return false
+}
+
+// upperFirst uppercases the first character of a given string.
+func upperFirst(s string) string {
+ if len(s) == 0 {
+ return s
+ }
+ out := []rune(s)
+ out[0] = unicode.ToUpper(out[0])
+ return string(out)
+}
diff --git a/ent/admin/handler.go b/ent/admin/handler.go
new file mode 100644
index 0000000..e52ec12
--- /dev/null
+++ b/ent/admin/handler.go
@@ -0,0 +1,319 @@
+// Code generated by ent, DO NOT EDIT.
+package admin
+
+import (
+ "fmt"
+ "net/url"
+ "strconv"
+ "time"
+
+ "entgo.io/ent/dialect/sql"
+ "github.com/labstack/echo/v4"
+
+ "github.com/camzawacki/personal-site/ent"
+ "github.com/camzawacki/personal-site/ent/passwordtoken"
+ "github.com/camzawacki/personal-site/ent/user"
+)
+
+const dateTimeFormat = "2006-01-02T15:04:05"
+const dateTimeFormatNoSeconds = "2006-01-02T15:04"
+
+type Handler struct {
+ client *ent.Client
+ Config HandlerConfig
+}
+
+func NewHandler(client *ent.Client, cfg HandlerConfig) *Handler {
+ return &Handler{
+ client: client,
+ Config: cfg,
+ }
+}
+
+func (h *Handler) Create(ctx echo.Context, entityType EntityType) error {
+ switch entityType.(type) {
+ 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 EntityType, id int) (url.Values, error) {
+ switch entityType.(type) {
+ case *PasswordToken:
+ return h.PasswordTokenGet(ctx, id)
+ case *User:
+ return h.UserGet(ctx, id)
+ default:
+ return nil, fmt.Errorf("unsupported entity type: %s", entityType)
+ }
+}
+
+func (h *Handler) Delete(ctx echo.Context, entityType EntityType, id int) error {
+ switch entityType.(type) {
+ 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 EntityType, id int) error {
+ switch entityType.(type) {
+ 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 EntityType) (*EntityList, error) {
+ switch entityType.(type) {
+ 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 := h.bind(ctx, &payload); err != nil {
+ return err
+ }
+
+ op := h.client.PasswordToken.Create()
+ if payload.Token != nil {
+ op.SetToken(*payload.Token)
+ }
+ op.SetUserID(payload.UserID)
+ if payload.CreatedAt != nil {
+ op.SetCreatedAt(*payload.CreatedAt)
+ }
+ _, 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 = h.bind(ctx, &payload); err != nil {
+ return err
+ }
+
+ op := entity.Update()
+ if payload.Token != nil {
+ op.SetToken(*payload.Token)
+ }
+ op.SetUserID(payload.UserID)
+ if payload.CreatedAt == nil {
+ var empty time.Time
+ op.SetCreatedAt(empty)
+ } else {
+ op.SetCreatedAt(*payload.CreatedAt)
+ }
+ _, 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) {
+ page, offset := h.getPageAndOffset(ctx)
+ res, err := h.client.PasswordToken.
+ Query().
+ Limit(h.Config.ItemsPerPage + 1).
+ Offset(offset).
+ Order(passwordtoken.ByID(sql.OrderDesc())).
+ All(ctx.Request().Context())
+
+ if err != nil {
+ return nil, err
+ }
+
+ list := &EntityList{
+ Columns: []string{
+ "User ID",
+ "Created at",
+ },
+ Entities: make([]EntityValues, 0, len(res)),
+ Page: page,
+ HasNextPage: len(res) > h.Config.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].UserID),
+ res[i].CreatedAt.Format(h.Config.TimeFormat),
+ },
+ })
+ }
+
+ return list, err
+}
+
+func (h *Handler) PasswordTokenGet(ctx echo.Context, id int) (url.Values, error) {
+ entity, err := h.client.PasswordToken.Get(ctx.Request().Context(), id)
+ if err != nil {
+ return nil, err
+ }
+
+ v := url.Values{}
+ v.Set("user_id", fmt.Sprint(entity.UserID))
+ v.Set("created_at", entity.CreatedAt.Format(dateTimeFormat))
+ return v, err
+}
+
+func (h *Handler) UserCreate(ctx echo.Context) error {
+ var payload User
+ if err := h.bind(ctx, &payload); err != nil {
+ return err
+ }
+
+ op := h.client.User.Create()
+ op.SetName(payload.Name)
+ op.SetEmail(payload.Email)
+ if payload.Password != nil {
+ op.SetPassword(*payload.Password)
+ }
+ op.SetVerified(payload.Verified)
+ op.SetAdmin(payload.Admin)
+ if payload.CreatedAt != nil {
+ 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 = h.bind(ctx, &payload); err != nil {
+ return err
+ }
+
+ op := entity.Update()
+ op.SetName(payload.Name)
+ op.SetEmail(payload.Email)
+ if payload.Password != nil {
+ op.SetPassword(*payload.Password)
+ }
+ op.SetVerified(payload.Verified)
+ op.SetAdmin(payload.Admin)
+ _, 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) {
+ page, offset := h.getPageAndOffset(ctx)
+ res, err := h.client.User.
+ Query().
+ Limit(h.Config.ItemsPerPage + 1).
+ Offset(offset).
+ Order(user.ByID(sql.OrderDesc())).
+ All(ctx.Request().Context())
+
+ if err != nil {
+ return nil, err
+ }
+
+ list := &EntityList{
+ Columns: []string{
+ "Name",
+ "Email",
+ "Verified",
+ "Admin",
+ "Created at",
+ },
+ Entities: make([]EntityValues, 0, len(res)),
+ Page: page,
+ HasNextPage: len(res) > h.Config.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].Admin),
+ res[i].CreatedAt.Format(h.Config.TimeFormat),
+ },
+ })
+ }
+
+ return list, err
+}
+
+func (h *Handler) UserGet(ctx echo.Context, id int) (url.Values, error) {
+ entity, err := h.client.User.Get(ctx.Request().Context(), id)
+ if err != nil {
+ return nil, err
+ }
+
+ v := url.Values{}
+ v.Set("name", entity.Name)
+ v.Set("email", entity.Email)
+ v.Set("verified", fmt.Sprint(entity.Verified))
+ v.Set("admin", fmt.Sprint(entity.Admin))
+ return v, err
+}
+
+func (h *Handler) getPageAndOffset(ctx echo.Context) (int, int) {
+ if page, err := strconv.Atoi(ctx.QueryParam(h.Config.PageQueryKey)); err == nil {
+ if page > 1 {
+ return page, (page - 1) * h.Config.ItemsPerPage
+ }
+ }
+ return 1, 0
+}
+
+func (h *Handler) bind(ctx echo.Context, entity any) error {
+ // Echo requires some pre-processing of form values to avoid problems.
+ for k, v := range ctx.Request().Form {
+ // Remove empty field values so Echo's bind does not fail when trying to parse things like
+ // times, etc.
+ if len(v) == 1 && len(v[0]) == 0 {
+ delete(ctx.Request().Form, k)
+ continue
+ }
+
+ // Echo expects datetime values to be in a certain format but that does not align with the datetime-local
+ // HTML form element format, so we will attempt to convert it here.
+ for _, format := range []string{dateTimeFormatNoSeconds, dateTimeFormat} {
+ if t, err := time.Parse(format, v[0]); err == nil {
+ ctx.Request().Form[k][0] = t.Format(time.RFC3339)
+ break
+ }
+ }
+ }
+ return ctx.Bind(entity)
+}
diff --git a/ent/admin/schema.go b/ent/admin/schema.go
new file mode 100644
index 0000000..8657b4c
--- /dev/null
+++ b/ent/admin/schema.go
@@ -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 []string
+}
+
+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,
+ },
+}
diff --git a/ent/admin/templates/handler.tmpl b/ent/admin/templates/handler.tmpl
new file mode 100644
index 0000000..e373ca4
--- /dev/null
+++ b/ent/admin/templates/handler.tmpl
@@ -0,0 +1,262 @@
+{{/* 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"
+ "net/url"
+ "strconv"
+
+ "entgo.io/ent/dialect/sql"
+ "github.com/labstack/echo/v4"
+
+ "{{ $.Config.Package }}"
+ {{- range $n := $.Nodes }}
+ "{{ $.Config.Package }}/{{ $n.Package }}"
+ {{- end }}
+ )
+
+ const dateTimeFormat = "2006-01-02T15:04:05"
+ const dateTimeFormatNoSeconds = "2006-01-02T15:04"
+
+ type Handler struct {
+ client *{{ $pkg }}.Client
+ Config HandlerConfig
+ }
+
+ func NewHandler(client *{{ $pkg }}.Client, cfg HandlerConfig) *Handler {
+ return &Handler{
+ client: client,
+ Config: cfg,
+ }
+ }
+
+ func (h *Handler) Create(ctx echo.Context, entityType EntityType) error {
+ switch entityType.(type) {
+ {{- 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 EntityType, id int) (url.Values, error) {
+ switch entityType.(type) {
+ {{- range $n := $.Nodes }}
+ case *{{ $n.Name }}:
+ return h.{{ $n.Name }}Get(ctx, id)
+ {{- end }}
+ default:
+ return nil, fmt.Errorf("unsupported entity type: %s", entityType)
+ }
+ }
+
+ func (h *Handler) Delete(ctx echo.Context, entityType EntityType, id int) error {
+ switch entityType.(type) {
+ {{- 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 EntityType, id int) error {
+ switch entityType.(type) {
+ {{- 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 EntityType) (*EntityList, error) {
+ switch entityType.(type) {
+ {{- 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 := h.bind(ctx, &payload); err != nil {
+ return err
+ }
+
+ op := h.client.{{ $n.Name }}.Create()
+ {{- range $f := $n.Fields }}
+ {{- if (fieldIsPointer $f) }}
+ if payload.{{ fieldName $f.Name }} != nil {
+ op.Set{{ fieldName $f.Name }}(*payload.{{ fieldName $f.Name }})
+ }
+ {{- else }}
+ op.Set{{ fieldName $f.Name }}(payload.{{ fieldName $f.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 = h.bind(ctx, &payload); err != nil {
+ return err
+ }
+
+ op := entity.Update()
+ {{- range $f := $n.Fields }}
+ {{- if not $f.Immutable }}
+ {{- if $f.Sensitive }}
+ if payload.{{ fieldName $f.Name }} != nil {
+ op.Set{{ fieldName $f.Name }}(*payload.{{ fieldName $f.Name }})
+ }
+ {{- else if $f.Nillable }}
+ op.SetNillable{{ fieldName $f.Name }}(payload.{{ fieldName $f.Name }})
+ {{- else if $f.Optional }}
+ if payload.{{ fieldName $f.Name }} == nil {
+ op.Clear{{ fieldName $f.Name }}()
+ } else {
+ op.Set{{ fieldName $f.Name }}(*payload.{{ fieldName $f.Name }})
+ }
+ {{- else if (fieldIsPointer $f) }}
+ if payload.{{ fieldName $f.Name }} == nil {
+ var empty {{ $f.Type }}
+ op.Set{{ fieldName $f.Name }}(empty)
+ } else {
+ op.Set{{ fieldName $f.Name }}(*payload.{{ fieldName $f.Name }})
+ }
+ {{- else }}
+ op.Set{{ fieldName $f.Name }}(payload.{{ fieldName $f.Name }})
+ {{- end }}
+ {{- 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) {
+ page, offset := h.getPageAndOffset(ctx)
+ res, err := h.client.{{ $n.Name }}.
+ Query().
+ Limit(h.Config.ItemsPerPage+1).
+ Offset(offset).
+ 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 }}
+ },
+ Entities: make([]EntityValues, 0, len(res)),
+ Page: page,
+ HasNextPage: len(res) > h.Config.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 if eq $f.Type.String "time.Time" }}
+ res[i].{{ fieldName $f.Name }}.Format(h.Config.TimeFormat),
+ {{- else }}
+ fmt.Sprint(res[i].{{ fieldName $f.Name }}),
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ },
+ })
+ }
+
+ return list, err
+ }
+
+ func (h *Handler) {{ $n.Name }}Get(ctx echo.Context, id int) (url.Values, error) {
+ entity, err := h.client.{{ $n.Name }}.Get(ctx.Request().Context(), id)
+ if err != nil {
+ return nil, err
+ }
+
+ v := url.Values{}
+ {{- range $f := $n.Fields }}
+ {{- if and (not $f.Sensitive) (not $f.Immutable) }}
+ {{- if eq $f.Type.String "string" }}
+ v.Set("{{ $f.Name }}", entity.{{ fieldName $f.Name }})
+ {{- else if eq $f.Type.String "time.Time" }}
+ v.Set("{{ $f.Name }}", entity.{{ fieldName $f.Name }}.Format(dateTimeFormat))
+ {{- else }}
+ v.Set("{{ $f.Name }}", fmt.Sprint(entity.{{ fieldName $f.Name }}))
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ return v, err
+ }
+ {{ end }}
+
+ func (h *Handler) getPageAndOffset(ctx echo.Context) (int, int) {
+ if page, err := strconv.Atoi(ctx.QueryParam(h.Config.PageQueryKey)); err == nil {
+ if page > 1 {
+ return page, (page-1) * h.Config.ItemsPerPage
+ }
+ }
+ return 1, 0
+ }
+
+ func (h *Handler) bind(ctx echo.Context, entity any) error {
+ // Echo requires some pre-processing of form values to avoid problems.
+ for k, v := range ctx.Request().Form {
+ // Remove empty field values so Echo's bind does not fail when trying to parse things like
+ // times, etc.
+ if len(v) == 1 && len(v[0]) == 0 {
+ delete(ctx.Request().Form, k)
+ continue
+ }
+
+ // Echo expects datetime values to be in a certain format but that does not align with the datetime-local
+ // HTML form element format, so we will attempt to convert it here.
+ for _, format := range []string{dateTimeFormatNoSeconds, dateTimeFormat} {
+ if t, err := time.Parse(format, v[0]); err == nil {
+ ctx.Request().Form[k][0] = t.Format(time.RFC3339)
+ break
+ }
+ }
+ }
+ return ctx.Bind(entity)
+ }
+
+{{ end }}
\ No newline at end of file
diff --git a/ent/admin/templates/schema.tmpl b/ent/admin/templates/schema.tmpl
new file mode 100644
index 0000000..752c631
--- /dev/null
+++ b/ent/admin/templates/schema.tmpl
@@ -0,0 +1,51 @@
+{{/* 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 []string
+ }
+
+
+ {{- 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: []string{
+ {{- range $e := $f.Enums }}
+ "{{ $e.Value }}",
+ {{- end }}
+ },
+ {{- else }}
+ Enums: nil,
+ {{- end }}
+ },
+ {{- end }}
+ }
+ {{ end }}
+
+{{ end }}
\ No newline at end of file
diff --git a/ent/admin/templates/types.tmpl b/ent/admin/templates/types.tmpl
new file mode 100644
index 0000000..e6c3f83
--- /dev/null
+++ b/ent/admin/templates/types.tmpl
@@ -0,0 +1,56 @@
+{{/* 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 {
+ {{- range $f := $n.Fields }}
+ {{ fieldName $f.Name }} {{ if (fieldIsPointer $f) }}*{{ end }}{{ $f.Type }} `form:"{{ $f.Name }}"`
+ {{- end }}
+ }
+
+ func (e *{{ $n.Name }}) GetName() string {
+ return Name{{ $n.Name }}
+ }
+
+ func (e *{{ $n.Name }}) GetSchema() []*FieldSchema {
+ return fields{{ $n.Name }}
+ }
+ {{ end }}
+
+ type EntityType interface {
+ GetName() string
+ GetSchema() []*FieldSchema
+ }
+
+ var entityTypes = []EntityType{
+ {{- range $n := $.Nodes }}
+ &{{ $n.Name }}{},
+ {{- end }}
+ }
+
+ type EntityList struct {
+ Columns []string
+ Entities []EntityValues
+ Page int
+ HasNextPage bool
+ }
+
+ type EntityValues struct {
+ ID int
+ Values []string
+ }
+
+ type HandlerConfig struct {
+ ItemsPerPage int
+ PageQueryKey string
+ TimeFormat string
+ }
+
+ func GetEntityTypes() []EntityType {
+ return entityTypes
+ }
+{{ end }}
\ No newline at end of file
diff --git a/ent/admin/types.go b/ent/admin/types.go
new file mode 100644
index 0000000..54f3a52
--- /dev/null
+++ b/ent/admin/types.go
@@ -0,0 +1,67 @@
+// Code generated by ent, DO NOT EDIT.
+package admin
+
+import "time"
+
+type PasswordToken struct {
+ Token *string `form:"token"`
+ UserID int `form:"user_id"`
+ CreatedAt *time.Time `form:"created_at"`
+}
+
+func (e *PasswordToken) GetName() string {
+ return NamePasswordToken
+}
+
+func (e *PasswordToken) GetSchema() []*FieldSchema {
+ return fieldsPasswordToken
+}
+
+type User struct {
+ Name string `form:"name"`
+ Email string `form:"email"`
+ Password *string `form:"password"`
+ Verified bool `form:"verified"`
+ Admin bool `form:"admin"`
+ 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 {
+ Columns []string
+ Entities []EntityValues
+ Page int
+ HasNextPage bool
+}
+
+type EntityValues struct {
+ ID int
+ Values []string
+}
+
+type HandlerConfig struct {
+ ItemsPerPage int
+ PageQueryKey string
+ TimeFormat string
+}
+
+func GetEntityTypes() []EntityType {
+ return entityTypes
+}
diff --git a/ent/client.go b/ent/client.go
index 59dc9fc..d864e3e 100644
--- a/ent/client.go
+++ b/ent/client.go
@@ -1,20 +1,22 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
+ "errors"
"fmt"
"log"
+ "reflect"
- "goweb/ent/migrate"
-
- "goweb/ent/passwordtoken"
- "goweb/ent/user"
+ "github.com/camzawacki/personal-site/ent/migrate"
+ "entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
+ "github.com/camzawacki/personal-site/ent/passwordtoken"
+ "github.com/camzawacki/personal-site/ent/user"
)
// Client is the client that holds all ent builders.
@@ -30,9 +32,7 @@ type Client struct {
// NewClient creates a new client configured with the given options.
func NewClient(opts ...Option) *Client {
- cfg := config{log: log.Println, hooks: &hooks{}}
- cfg.options(opts...)
- client := &Client{config: cfg}
+ client := &Client{config: newConfig(opts...)}
client.init()
return client
}
@@ -43,6 +43,62 @@ func (c *Client) init() {
c.User = NewUserClient(c.config)
}
+type (
+ // config is the configuration for the client and its builder.
+ config struct {
+ // driver used for executing database requests.
+ driver dialect.Driver
+ // debug enable a debug logging.
+ debug bool
+ // log used for logging on debug mode.
+ log func(...any)
+ // hooks to execute on mutations.
+ hooks *hooks
+ // interceptors to execute on queries.
+ inters *inters
+ }
+ // Option function to configure the client.
+ Option func(*config)
+)
+
+// newConfig creates a new config for the client.
+func newConfig(opts ...Option) config {
+ cfg := config{log: log.Println, hooks: &hooks{}, inters: &inters{}}
+ cfg.options(opts...)
+ return cfg
+}
+
+// options applies the options on the config object.
+func (c *config) options(opts ...Option) {
+ for _, opt := range opts {
+ opt(c)
+ }
+ if c.debug {
+ c.driver = dialect.Debug(c.driver, c.log)
+ }
+}
+
+// Debug enables debug logging on the ent.Driver.
+func Debug() Option {
+ return func(c *config) {
+ c.debug = true
+ }
+}
+
+// Log sets the logging function for debug mode.
+func Log(fn func(...any)) Option {
+ return func(c *config) {
+ c.log = fn
+ }
+}
+
+// Driver configures the client driver.
+func Driver(driver dialect.Driver) Option {
+ return func(c *config) {
+ c.driver = driver
+ }
+}
+
// Open opens a database/sql.DB specified by the driver name and
// the data source name, and returns a new client attached to it.
// Optional parameters can be added for configuring the client.
@@ -59,11 +115,14 @@ func Open(driverName, dataSourceName string, options ...Option) (*Client, error)
}
}
+// ErrTxStarted is returned when trying to start a new transaction from a transactional client.
+var ErrTxStarted = errors.New("ent: cannot start a transaction within a transaction")
+
// Tx returns a new transactional client. The provided context
// is used until the transaction is committed or rolled back.
func (c *Client) Tx(ctx context.Context) (*Tx, error) {
if _, ok := c.driver.(*txDriver); ok {
- return nil, fmt.Errorf("ent: cannot start a transaction within a transaction")
+ return nil, ErrTxStarted
}
tx, err := newTx(ctx, c.driver)
if err != nil {
@@ -82,7 +141,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
// BeginTx returns a transactional client with specified options.
func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) {
if _, ok := c.driver.(*txDriver); ok {
- return nil, fmt.Errorf("ent: cannot start a transaction within a transaction")
+ return nil, errors.New("ent: cannot start a transaction within a transaction")
}
tx, err := c.driver.(interface {
BeginTx(context.Context, *sql.TxOptions) (dialect.Tx, error)
@@ -93,6 +152,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
cfg := c.config
cfg.driver = &txDriver{tx: tx, drv: c.driver}
return &Tx{
+ ctx: ctx,
config: cfg,
PasswordToken: NewPasswordTokenClient(cfg),
User: NewUserClient(cfg),
@@ -105,7 +165,6 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
// PasswordToken.
// Query().
// Count(ctx)
-//
func (c *Client) Debug() *Client {
if c.debug {
return c
@@ -129,6 +188,25 @@ func (c *Client) Use(hooks ...Hook) {
c.User.Use(hooks...)
}
+// Intercept adds the query interceptors to all the entity clients.
+// In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`.
+func (c *Client) Intercept(interceptors ...Interceptor) {
+ c.PasswordToken.Intercept(interceptors...)
+ c.User.Intercept(interceptors...)
+}
+
+// Mutate implements the ent.Mutator interface.
+func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
+ switch m := m.(type) {
+ case *PasswordTokenMutation:
+ return c.PasswordToken.mutate(ctx, m)
+ case *UserMutation:
+ return c.User.mutate(ctx, m)
+ default:
+ return nil, fmt.Errorf("ent: unknown mutation type %T", m)
+ }
+}
+
// PasswordTokenClient is a client for the PasswordToken schema.
type PasswordTokenClient struct {
config
@@ -145,7 +223,13 @@ func (c *PasswordTokenClient) Use(hooks ...Hook) {
c.hooks.PasswordToken = append(c.hooks.PasswordToken, hooks...)
}
-// Create returns a create builder for PasswordToken.
+// Intercept adds a list of query interceptors to the interceptors stack.
+// A call to `Intercept(f, g, h)` equals to `passwordtoken.Intercept(f(g(h())))`.
+func (c *PasswordTokenClient) Intercept(interceptors ...Interceptor) {
+ c.inters.PasswordToken = append(c.inters.PasswordToken, interceptors...)
+}
+
+// Create returns a builder for creating a PasswordToken entity.
func (c *PasswordTokenClient) Create() *PasswordTokenCreate {
mutation := newPasswordTokenMutation(c.config, OpCreate)
return &PasswordTokenCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
@@ -156,6 +240,21 @@ func (c *PasswordTokenClient) CreateBulk(builders ...*PasswordTokenCreate) *Pass
return &PasswordTokenCreateBulk{config: c.config, builders: builders}
}
+// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
+// a builder and applies setFunc on it.
+func (c *PasswordTokenClient) MapCreateBulk(slice any, setFunc func(*PasswordTokenCreate, int)) *PasswordTokenCreateBulk {
+ rv := reflect.ValueOf(slice)
+ if rv.Kind() != reflect.Slice {
+ return &PasswordTokenCreateBulk{err: fmt.Errorf("calling to PasswordTokenClient.MapCreateBulk with wrong type %T, need slice", slice)}
+ }
+ builders := make([]*PasswordTokenCreate, rv.Len())
+ for i := 0; i < rv.Len(); i++ {
+ builders[i] = c.Create()
+ setFunc(builders[i], i)
+ }
+ return &PasswordTokenCreateBulk{config: c.config, builders: builders}
+}
+
// Update returns an update builder for PasswordToken.
func (c *PasswordTokenClient) Update() *PasswordTokenUpdate {
mutation := newPasswordTokenMutation(c.config, OpUpdate)
@@ -163,8 +262,8 @@ func (c *PasswordTokenClient) Update() *PasswordTokenUpdate {
}
// UpdateOne returns an update builder for the given entity.
-func (c *PasswordTokenClient) UpdateOne(pt *PasswordToken) *PasswordTokenUpdateOne {
- mutation := newPasswordTokenMutation(c.config, OpUpdateOne, withPasswordToken(pt))
+func (c *PasswordTokenClient) UpdateOne(_m *PasswordToken) *PasswordTokenUpdateOne {
+ mutation := newPasswordTokenMutation(c.config, OpUpdateOne, withPasswordToken(_m))
return &PasswordTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
@@ -180,12 +279,12 @@ func (c *PasswordTokenClient) Delete() *PasswordTokenDelete {
return &PasswordTokenDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
-// DeleteOne returns a delete builder for the given entity.
-func (c *PasswordTokenClient) DeleteOne(pt *PasswordToken) *PasswordTokenDeleteOne {
- return c.DeleteOneID(pt.ID)
+// DeleteOne returns a builder for deleting the given entity.
+func (c *PasswordTokenClient) DeleteOne(_m *PasswordToken) *PasswordTokenDeleteOne {
+ return c.DeleteOneID(_m.ID)
}
-// DeleteOneID returns a delete builder for the given id.
+// DeleteOneID returns a builder for deleting the given entity by its id.
func (c *PasswordTokenClient) DeleteOneID(id int) *PasswordTokenDeleteOne {
builder := c.Delete().Where(passwordtoken.ID(id))
builder.mutation.id = &id
@@ -197,6 +296,8 @@ func (c *PasswordTokenClient) DeleteOneID(id int) *PasswordTokenDeleteOne {
func (c *PasswordTokenClient) Query() *PasswordTokenQuery {
return &PasswordTokenQuery{
config: c.config,
+ ctx: &QueryContext{Type: TypePasswordToken},
+ inters: c.Interceptors(),
}
}
@@ -215,16 +316,16 @@ func (c *PasswordTokenClient) GetX(ctx context.Context, id int) *PasswordToken {
}
// QueryUser queries the user edge of a PasswordToken.
-func (c *PasswordTokenClient) QueryUser(pt *PasswordToken) *UserQuery {
- query := &UserQuery{config: c.config}
- query.path = func(ctx context.Context) (fromV *sql.Selector, _ error) {
- id := pt.ID
+func (c *PasswordTokenClient) QueryUser(_m *PasswordToken) *UserQuery {
+ query := (&UserClient{config: c.config}).Query()
+ query.path = func(context.Context) (fromV *sql.Selector, _ error) {
+ id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(passwordtoken.Table, passwordtoken.FieldID, id),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2O, false, passwordtoken.UserTable, passwordtoken.UserColumn),
)
- fromV = sqlgraph.Neighbors(pt.driver.Dialect(), step)
+ fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
@@ -232,7 +333,28 @@ func (c *PasswordTokenClient) QueryUser(pt *PasswordToken) *UserQuery {
// Hooks returns the client hooks.
func (c *PasswordTokenClient) Hooks() []Hook {
- return c.hooks.PasswordToken
+ hooks := c.hooks.PasswordToken
+ return append(hooks[:len(hooks):len(hooks)], passwordtoken.Hooks[:]...)
+}
+
+// Interceptors returns the client interceptors.
+func (c *PasswordTokenClient) Interceptors() []Interceptor {
+ return c.inters.PasswordToken
+}
+
+func (c *PasswordTokenClient) mutate(ctx context.Context, m *PasswordTokenMutation) (Value, error) {
+ switch m.Op() {
+ case OpCreate:
+ return (&PasswordTokenCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
+ case OpUpdate:
+ return (&PasswordTokenUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
+ case OpUpdateOne:
+ return (&PasswordTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
+ case OpDelete, OpDeleteOne:
+ return (&PasswordTokenDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
+ default:
+ return nil, fmt.Errorf("ent: unknown PasswordToken mutation op: %q", m.Op())
+ }
}
// UserClient is a client for the User schema.
@@ -251,7 +373,13 @@ func (c *UserClient) Use(hooks ...Hook) {
c.hooks.User = append(c.hooks.User, hooks...)
}
-// Create returns a create builder for User.
+// Intercept adds a list of query interceptors to the interceptors stack.
+// A call to `Intercept(f, g, h)` equals to `user.Intercept(f(g(h())))`.
+func (c *UserClient) Intercept(interceptors ...Interceptor) {
+ c.inters.User = append(c.inters.User, interceptors...)
+}
+
+// Create returns a builder for creating a User entity.
func (c *UserClient) Create() *UserCreate {
mutation := newUserMutation(c.config, OpCreate)
return &UserCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
@@ -262,6 +390,21 @@ func (c *UserClient) CreateBulk(builders ...*UserCreate) *UserCreateBulk {
return &UserCreateBulk{config: c.config, builders: builders}
}
+// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
+// a builder and applies setFunc on it.
+func (c *UserClient) MapCreateBulk(slice any, setFunc func(*UserCreate, int)) *UserCreateBulk {
+ rv := reflect.ValueOf(slice)
+ if rv.Kind() != reflect.Slice {
+ return &UserCreateBulk{err: fmt.Errorf("calling to UserClient.MapCreateBulk with wrong type %T, need slice", slice)}
+ }
+ builders := make([]*UserCreate, rv.Len())
+ for i := 0; i < rv.Len(); i++ {
+ builders[i] = c.Create()
+ setFunc(builders[i], i)
+ }
+ return &UserCreateBulk{config: c.config, builders: builders}
+}
+
// Update returns an update builder for User.
func (c *UserClient) Update() *UserUpdate {
mutation := newUserMutation(c.config, OpUpdate)
@@ -269,8 +412,8 @@ func (c *UserClient) Update() *UserUpdate {
}
// UpdateOne returns an update builder for the given entity.
-func (c *UserClient) UpdateOne(u *User) *UserUpdateOne {
- mutation := newUserMutation(c.config, OpUpdateOne, withUser(u))
+func (c *UserClient) UpdateOne(_m *User) *UserUpdateOne {
+ mutation := newUserMutation(c.config, OpUpdateOne, withUser(_m))
return &UserUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
@@ -286,12 +429,12 @@ func (c *UserClient) Delete() *UserDelete {
return &UserDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
-// DeleteOne returns a delete builder for the given entity.
-func (c *UserClient) DeleteOne(u *User) *UserDeleteOne {
- return c.DeleteOneID(u.ID)
+// DeleteOne returns a builder for deleting the given entity.
+func (c *UserClient) DeleteOne(_m *User) *UserDeleteOne {
+ return c.DeleteOneID(_m.ID)
}
-// DeleteOneID returns a delete builder for the given id.
+// DeleteOneID returns a builder for deleting the given entity by its id.
func (c *UserClient) DeleteOneID(id int) *UserDeleteOne {
builder := c.Delete().Where(user.ID(id))
builder.mutation.id = &id
@@ -303,6 +446,8 @@ func (c *UserClient) DeleteOneID(id int) *UserDeleteOne {
func (c *UserClient) Query() *UserQuery {
return &UserQuery{
config: c.config,
+ ctx: &QueryContext{Type: TypeUser},
+ inters: c.Interceptors(),
}
}
@@ -321,16 +466,16 @@ func (c *UserClient) GetX(ctx context.Context, id int) *User {
}
// QueryOwner queries the owner edge of a User.
-func (c *UserClient) QueryOwner(u *User) *PasswordTokenQuery {
- query := &PasswordTokenQuery{config: c.config}
- query.path = func(ctx context.Context) (fromV *sql.Selector, _ error) {
- id := u.ID
+func (c *UserClient) QueryOwner(_m *User) *PasswordTokenQuery {
+ query := (&PasswordTokenClient{config: c.config}).Query()
+ query.path = func(context.Context) (fromV *sql.Selector, _ error) {
+ id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID, id),
sqlgraph.To(passwordtoken.Table, passwordtoken.FieldID),
sqlgraph.Edge(sqlgraph.O2M, true, user.OwnerTable, user.OwnerColumn),
)
- fromV = sqlgraph.Neighbors(u.driver.Dialect(), step)
+ fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
@@ -341,3 +486,33 @@ func (c *UserClient) Hooks() []Hook {
hooks := c.hooks.User
return append(hooks[:len(hooks):len(hooks)], user.Hooks[:]...)
}
+
+// Interceptors returns the client interceptors.
+func (c *UserClient) Interceptors() []Interceptor {
+ return c.inters.User
+}
+
+func (c *UserClient) mutate(ctx context.Context, m *UserMutation) (Value, error) {
+ switch m.Op() {
+ case OpCreate:
+ return (&UserCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
+ case OpUpdate:
+ return (&UserUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
+ case OpUpdateOne:
+ return (&UserUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
+ case OpDelete, OpDeleteOne:
+ return (&UserDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
+ default:
+ return nil, fmt.Errorf("ent: unknown User mutation op: %q", m.Op())
+ }
+}
+
+// hooks and interceptors per client, for fast access.
+type (
+ hooks struct {
+ PasswordToken, User []ent.Hook
+ }
+ inters struct {
+ PasswordToken, User []ent.Interceptor
+ }
+)
diff --git a/ent/config.go b/ent/config.go
deleted file mode 100644
index a7d1d36..0000000
--- a/ent/config.go
+++ /dev/null
@@ -1,60 +0,0 @@
-// Code generated by entc, DO NOT EDIT.
-
-package ent
-
-import (
- "entgo.io/ent"
- "entgo.io/ent/dialect"
-)
-
-// Option function to configure the client.
-type Option func(*config)
-
-// Config is the configuration for the client and its builder.
-type config struct {
- // driver used for executing database requests.
- driver dialect.Driver
- // debug enable a debug logging.
- debug bool
- // log used for logging on debug mode.
- log func(...interface{})
- // hooks to execute on mutations.
- hooks *hooks
-}
-
-// hooks per client, for fast access.
-type hooks struct {
- PasswordToken []ent.Hook
- User []ent.Hook
-}
-
-// Options applies the options on the config object.
-func (c *config) options(opts ...Option) {
- for _, opt := range opts {
- opt(c)
- }
- if c.debug {
- c.driver = dialect.Debug(c.driver, c.log)
- }
-}
-
-// Debug enables debug logging on the ent.Driver.
-func Debug() Option {
- return func(c *config) {
- c.debug = true
- }
-}
-
-// Log sets the logging function for debug mode.
-func Log(fn func(...interface{})) Option {
- return func(c *config) {
- c.log = fn
- }
-}
-
-// Driver configures the client driver.
-func Driver(driver dialect.Driver) Option {
- return func(c *config) {
- c.driver = driver
- }
-}
diff --git a/ent/context.go b/ent/context.go
deleted file mode 100644
index 0840726..0000000
--- a/ent/context.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Code generated by entc, DO NOT EDIT.
-
-package ent
-
-import (
- "context"
-)
-
-type clientCtxKey struct{}
-
-// FromContext returns a Client stored inside a context, or nil if there isn't one.
-func FromContext(ctx context.Context) *Client {
- c, _ := ctx.Value(clientCtxKey{}).(*Client)
- return c
-}
-
-// NewContext returns a new context with the given Client attached.
-func NewContext(parent context.Context, c *Client) context.Context {
- return context.WithValue(parent, clientCtxKey{}, c)
-}
-
-type txCtxKey struct{}
-
-// TxFromContext returns a Tx stored inside a context, or nil if there isn't one.
-func TxFromContext(ctx context.Context) *Tx {
- tx, _ := ctx.Value(txCtxKey{}).(*Tx)
- return tx
-}
-
-// NewTxContext returns a new context with the given Tx attached.
-func NewTxContext(parent context.Context, tx *Tx) context.Context {
- return context.WithValue(parent, txCtxKey{}, tx)
-}
diff --git a/ent/ent.go b/ent/ent.go
index 23d08c9..ddab432 100644
--- a/ent/ent.go
+++ b/ent/ent.go
@@ -1,58 +1,91 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
import (
+ "context"
"errors"
"fmt"
- "goweb/ent/passwordtoken"
- "goweb/ent/user"
+ "reflect"
+ "sync"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
+ "entgo.io/ent/dialect/sql/sqlgraph"
+ "github.com/camzawacki/personal-site/ent/passwordtoken"
+ "github.com/camzawacki/personal-site/ent/user"
)
// ent aliases to avoid import conflicts in user's code.
type (
- Op = ent.Op
- Hook = ent.Hook
- Value = ent.Value
- Query = ent.Query
- Policy = ent.Policy
- Mutator = ent.Mutator
- Mutation = ent.Mutation
- MutateFunc = ent.MutateFunc
+ Op = ent.Op
+ Hook = ent.Hook
+ Value = ent.Value
+ Query = ent.Query
+ QueryContext = ent.QueryContext
+ Querier = ent.Querier
+ QuerierFunc = ent.QuerierFunc
+ Interceptor = ent.Interceptor
+ InterceptFunc = ent.InterceptFunc
+ Traverser = ent.Traverser
+ TraverseFunc = ent.TraverseFunc
+ Policy = ent.Policy
+ Mutator = ent.Mutator
+ Mutation = ent.Mutation
+ MutateFunc = ent.MutateFunc
)
+type clientCtxKey struct{}
+
+// FromContext returns a Client stored inside a context, or nil if there isn't one.
+func FromContext(ctx context.Context) *Client {
+ c, _ := ctx.Value(clientCtxKey{}).(*Client)
+ return c
+}
+
+// NewContext returns a new context with the given Client attached.
+func NewContext(parent context.Context, c *Client) context.Context {
+ return context.WithValue(parent, clientCtxKey{}, c)
+}
+
+type txCtxKey struct{}
+
+// TxFromContext returns a Tx stored inside a context, or nil if there isn't one.
+func TxFromContext(ctx context.Context) *Tx {
+ tx, _ := ctx.Value(txCtxKey{}).(*Tx)
+ return tx
+}
+
+// NewTxContext returns a new context with the given Tx attached.
+func NewTxContext(parent context.Context, tx *Tx) context.Context {
+ return context.WithValue(parent, txCtxKey{}, tx)
+}
+
// OrderFunc applies an ordering on the sql selector.
+// Deprecated: Use Asc/Desc functions or the package builders instead.
type OrderFunc func(*sql.Selector)
-// columnChecker returns a function indicates if the column exists in the given column.
-func columnChecker(table string) func(string) error {
- checks := map[string]func(string) bool{
- passwordtoken.Table: passwordtoken.ValidColumn,
- user.Table: user.ValidColumn,
- }
- check, ok := checks[table]
- if !ok {
- return func(string) error {
- return fmt.Errorf("unknown table %q", table)
- }
- }
- return func(column string) error {
- if !check(column) {
- return fmt.Errorf("unknown column %q for table %q", column, table)
- }
- return nil
- }
+var (
+ initCheck sync.Once
+ columnCheck sql.ColumnCheck
+)
+
+// checkColumn checks if the column exists in the given table.
+func checkColumn(t, c string) error {
+ initCheck.Do(func() {
+ columnCheck = sql.NewColumnCheck(map[string]func(string) bool{
+ passwordtoken.Table: passwordtoken.ValidColumn,
+ user.Table: user.ValidColumn,
+ })
+ })
+ return columnCheck(t, c)
}
// Asc applies the given fields in ASC order.
-func Asc(fields ...string) OrderFunc {
+func Asc(fields ...string) func(*sql.Selector) {
return func(s *sql.Selector) {
- check := columnChecker(s.TableName())
for _, f := range fields {
- if err := check(f); err != nil {
+ if err := checkColumn(s.TableName(), f); err != nil {
s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)})
}
s.OrderBy(sql.Asc(s.C(f)))
@@ -61,11 +94,10 @@ func Asc(fields ...string) OrderFunc {
}
// Desc applies the given fields in DESC order.
-func Desc(fields ...string) OrderFunc {
+func Desc(fields ...string) func(*sql.Selector) {
return func(s *sql.Selector) {
- check := columnChecker(s.TableName())
for _, f := range fields {
- if err := check(f); err != nil {
+ if err := checkColumn(s.TableName(), f); err != nil {
s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)})
}
s.OrderBy(sql.Desc(s.C(f)))
@@ -81,7 +113,6 @@ type AggregateFunc func(*sql.Selector) string
// GroupBy(field1, field2).
// Aggregate(ent.As(ent.Sum(field1), "sum_field1"), (ent.As(ent.Sum(field2), "sum_field2")).
// Scan(ctx, &v)
-//
func As(fn AggregateFunc, end string) AggregateFunc {
return func(s *sql.Selector) string {
return sql.As(fn(s), end)
@@ -98,8 +129,7 @@ func Count() AggregateFunc {
// Max applies the "max" aggregation function on the given field of each group.
func Max(field string) AggregateFunc {
return func(s *sql.Selector) string {
- check := columnChecker(s.TableName())
- if err := check(field); err != nil {
+ if err := checkColumn(s.TableName(), field); err != nil {
s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)})
return ""
}
@@ -110,8 +140,7 @@ func Max(field string) AggregateFunc {
// Mean applies the "mean" aggregation function on the given field of each group.
func Mean(field string) AggregateFunc {
return func(s *sql.Selector) string {
- check := columnChecker(s.TableName())
- if err := check(field); err != nil {
+ if err := checkColumn(s.TableName(), field); err != nil {
s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)})
return ""
}
@@ -122,8 +151,7 @@ func Mean(field string) AggregateFunc {
// Min applies the "min" aggregation function on the given field of each group.
func Min(field string) AggregateFunc {
return func(s *sql.Selector) string {
- check := columnChecker(s.TableName())
- if err := check(field); err != nil {
+ if err := checkColumn(s.TableName(), field); err != nil {
s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)})
return ""
}
@@ -134,8 +162,7 @@ func Min(field string) AggregateFunc {
// Sum applies the "sum" aggregation function on the given field of each group.
func Sum(field string) AggregateFunc {
return func(s *sql.Selector) string {
- check := columnChecker(s.TableName())
- if err := check(field); err != nil {
+ if err := checkColumn(s.TableName(), field); err != nil {
s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)})
return ""
}
@@ -143,7 +170,7 @@ func Sum(field string) AggregateFunc {
}
}
-// ValidationError returns when validating a field fails.
+// ValidationError returns when validating a field or edge fails.
type ValidationError struct {
Name string // Field or edge name.
err error
@@ -259,3 +286,325 @@ func IsConstraintError(err error) bool {
var e *ConstraintError
return errors.As(err, &e)
}
+
+// selector embedded by the different Select/GroupBy builders.
+type selector struct {
+ label string
+ flds *[]string
+ fns []AggregateFunc
+ scan func(context.Context, any) error
+}
+
+// ScanX is like Scan, but panics if an error occurs.
+func (s *selector) ScanX(ctx context.Context, v any) {
+ if err := s.scan(ctx, v); err != nil {
+ panic(err)
+ }
+}
+
+// Strings returns list of strings from a selector. It is only allowed when selecting one field.
+func (s *selector) Strings(ctx context.Context) ([]string, error) {
+ if len(*s.flds) > 1 {
+ return nil, errors.New("ent: Strings is not achievable when selecting more than 1 field")
+ }
+ var v []string
+ if err := s.scan(ctx, &v); err != nil {
+ return nil, err
+ }
+ return v, nil
+}
+
+// StringsX is like Strings, but panics if an error occurs.
+func (s *selector) StringsX(ctx context.Context) []string {
+ v, err := s.Strings(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// String returns a single string from a selector. It is only allowed when selecting one field.
+func (s *selector) String(ctx context.Context) (_ string, err error) {
+ var v []string
+ if v, err = s.Strings(ctx); err != nil {
+ return
+ }
+ switch len(v) {
+ case 1:
+ return v[0], nil
+ case 0:
+ err = &NotFoundError{s.label}
+ default:
+ err = fmt.Errorf("ent: Strings returned %d results when one was expected", len(v))
+ }
+ return
+}
+
+// StringX is like String, but panics if an error occurs.
+func (s *selector) StringX(ctx context.Context) string {
+ v, err := s.String(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// Ints returns list of ints from a selector. It is only allowed when selecting one field.
+func (s *selector) Ints(ctx context.Context) ([]int, error) {
+ if len(*s.flds) > 1 {
+ return nil, errors.New("ent: Ints is not achievable when selecting more than 1 field")
+ }
+ var v []int
+ if err := s.scan(ctx, &v); err != nil {
+ return nil, err
+ }
+ return v, nil
+}
+
+// IntsX is like Ints, but panics if an error occurs.
+func (s *selector) IntsX(ctx context.Context) []int {
+ v, err := s.Ints(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// Int returns a single int from a selector. It is only allowed when selecting one field.
+func (s *selector) Int(ctx context.Context) (_ int, err error) {
+ var v []int
+ if v, err = s.Ints(ctx); err != nil {
+ return
+ }
+ switch len(v) {
+ case 1:
+ return v[0], nil
+ case 0:
+ err = &NotFoundError{s.label}
+ default:
+ err = fmt.Errorf("ent: Ints returned %d results when one was expected", len(v))
+ }
+ return
+}
+
+// IntX is like Int, but panics if an error occurs.
+func (s *selector) IntX(ctx context.Context) int {
+ v, err := s.Int(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// Float64s returns list of float64s from a selector. It is only allowed when selecting one field.
+func (s *selector) Float64s(ctx context.Context) ([]float64, error) {
+ if len(*s.flds) > 1 {
+ return nil, errors.New("ent: Float64s is not achievable when selecting more than 1 field")
+ }
+ var v []float64
+ if err := s.scan(ctx, &v); err != nil {
+ return nil, err
+ }
+ return v, nil
+}
+
+// Float64sX is like Float64s, but panics if an error occurs.
+func (s *selector) Float64sX(ctx context.Context) []float64 {
+ v, err := s.Float64s(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// Float64 returns a single float64 from a selector. It is only allowed when selecting one field.
+func (s *selector) Float64(ctx context.Context) (_ float64, err error) {
+ var v []float64
+ if v, err = s.Float64s(ctx); err != nil {
+ return
+ }
+ switch len(v) {
+ case 1:
+ return v[0], nil
+ case 0:
+ err = &NotFoundError{s.label}
+ default:
+ err = fmt.Errorf("ent: Float64s returned %d results when one was expected", len(v))
+ }
+ return
+}
+
+// Float64X is like Float64, but panics if an error occurs.
+func (s *selector) Float64X(ctx context.Context) float64 {
+ v, err := s.Float64(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// Bools returns list of bools from a selector. It is only allowed when selecting one field.
+func (s *selector) Bools(ctx context.Context) ([]bool, error) {
+ if len(*s.flds) > 1 {
+ return nil, errors.New("ent: Bools is not achievable when selecting more than 1 field")
+ }
+ var v []bool
+ if err := s.scan(ctx, &v); err != nil {
+ return nil, err
+ }
+ return v, nil
+}
+
+// BoolsX is like Bools, but panics if an error occurs.
+func (s *selector) BoolsX(ctx context.Context) []bool {
+ v, err := s.Bools(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// Bool returns a single bool from a selector. It is only allowed when selecting one field.
+func (s *selector) Bool(ctx context.Context) (_ bool, err error) {
+ var v []bool
+ if v, err = s.Bools(ctx); err != nil {
+ return
+ }
+ switch len(v) {
+ case 1:
+ return v[0], nil
+ case 0:
+ err = &NotFoundError{s.label}
+ default:
+ err = fmt.Errorf("ent: Bools returned %d results when one was expected", len(v))
+ }
+ return
+}
+
+// BoolX is like Bool, but panics if an error occurs.
+func (s *selector) BoolX(ctx context.Context) bool {
+ v, err := s.Bool(ctx)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// withHooks invokes the builder operation with the given hooks, if any.
+func withHooks[V Value, M any, PM interface {
+ *M
+ Mutation
+}](ctx context.Context, exec func(context.Context) (V, error), mutation PM, hooks []Hook) (value V, err error) {
+ if len(hooks) == 0 {
+ return exec(ctx)
+ }
+ var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
+ mutationT, ok := any(m).(PM)
+ if !ok {
+ return nil, fmt.Errorf("unexpected mutation type %T", m)
+ }
+ // Set the mutation to the builder.
+ *mutation = *mutationT
+ return exec(ctx)
+ })
+ for i := len(hooks) - 1; i >= 0; i-- {
+ if hooks[i] == nil {
+ return value, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)")
+ }
+ mut = hooks[i](mut)
+ }
+ v, err := mut.Mutate(ctx, mutation)
+ if err != nil {
+ return value, err
+ }
+ nv, ok := v.(V)
+ if !ok {
+ return value, fmt.Errorf("unexpected node type %T returned from %T", v, mutation)
+ }
+ return nv, nil
+}
+
+// setContextOp returns a new context with the given QueryContext attached (including its op) in case it does not exist.
+func setContextOp(ctx context.Context, qc *QueryContext, op string) context.Context {
+ if ent.QueryFromContext(ctx) == nil {
+ qc.Op = op
+ ctx = ent.NewQueryContext(ctx, qc)
+ }
+ return ctx
+}
+
+func querierAll[V Value, Q interface {
+ sqlAll(context.Context, ...queryHook) (V, error)
+}]() Querier {
+ return QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
+ query, ok := q.(Q)
+ if !ok {
+ return nil, fmt.Errorf("unexpected query type %T", q)
+ }
+ return query.sqlAll(ctx)
+ })
+}
+
+func querierCount[Q interface {
+ sqlCount(context.Context) (int, error)
+}]() Querier {
+ return QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
+ query, ok := q.(Q)
+ if !ok {
+ return nil, fmt.Errorf("unexpected query type %T", q)
+ }
+ return query.sqlCount(ctx)
+ })
+}
+
+func withInterceptors[V Value](ctx context.Context, q Query, qr Querier, inters []Interceptor) (v V, err error) {
+ for i := len(inters) - 1; i >= 0; i-- {
+ qr = inters[i].Intercept(qr)
+ }
+ rv, err := qr.Query(ctx, q)
+ if err != nil {
+ return v, err
+ }
+ vt, ok := rv.(V)
+ if !ok {
+ return v, fmt.Errorf("unexpected type %T returned from %T. expected type: %T", vt, q, v)
+ }
+ return vt, nil
+}
+
+func scanWithInterceptors[Q1 ent.Query, Q2 interface {
+ sqlScan(context.Context, Q1, any) error
+}](ctx context.Context, rootQuery Q1, selectOrGroup Q2, inters []Interceptor, v any) error {
+ rv := reflect.ValueOf(v)
+ var qr Querier = QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
+ query, ok := q.(Q1)
+ if !ok {
+ return nil, fmt.Errorf("unexpected query type %T", q)
+ }
+ if err := selectOrGroup.sqlScan(ctx, query, v); err != nil {
+ return nil, err
+ }
+ if k := rv.Kind(); k == reflect.Pointer && rv.Elem().CanInterface() {
+ return rv.Elem().Interface(), nil
+ }
+ return v, nil
+ })
+ for i := len(inters) - 1; i >= 0; i-- {
+ qr = inters[i].Intercept(qr)
+ }
+ vv, err := qr.Query(ctx, rootQuery)
+ if err != nil {
+ return err
+ }
+ switch rv2 := reflect.ValueOf(vv); {
+ case rv.IsNil(), rv2.IsNil(), rv.Kind() != reflect.Pointer:
+ case rv.Type() == rv2.Type():
+ rv.Elem().Set(rv2.Elem())
+ case rv.Elem().Type() == rv2.Type():
+ rv.Elem().Set(rv2)
+ }
+ return nil
+}
+
+// queryHook describes an internal hook for the different sqlAll methods.
+type queryHook func(context.Context, *sqlgraph.QuerySpec)
diff --git a/ent/entc.go b/ent/entc.go
new file mode 100644
index 0000000..32f1740
--- /dev/null
+++ b/ent/entc.go
@@ -0,0 +1,22 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "log"
+
+ "entgo.io/ent/entc"
+ "entgo.io/ent/entc/gen"
+ "github.com/camzawacki/personal-site/ent/admin"
+)
+
+func main() {
+ err := entc.Generate("./schema",
+ &gen.Config{},
+ entc.Extensions(&admin.Extension{}),
+ )
+ if err != nil {
+ log.Fatal("running ent codegen:", err)
+ }
+}
diff --git a/ent/enttest/enttest.go b/ent/enttest/enttest.go
index 65fa399..f849c03 100644
--- a/ent/enttest/enttest.go
+++ b/ent/enttest/enttest.go
@@ -1,14 +1,16 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package enttest
import (
"context"
- "goweb/ent"
+
+ "github.com/camzawacki/personal-site/ent"
// required by schema hooks.
- _ "goweb/ent/runtime"
+ _ "github.com/camzawacki/personal-site/ent/runtime"
"entgo.io/ent/dialect/sql/schema"
+ "github.com/camzawacki/personal-site/ent/migrate"
)
type (
@@ -16,7 +18,7 @@ type (
// testing.T and testing.B and used by enttest.
TestingT interface {
FailNow()
- Error(...interface{})
+ Error(...any)
}
// Option configures client creation.
@@ -58,10 +60,7 @@ func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Cl
t.Error(err)
t.FailNow()
}
- if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
- t.Error(err)
- t.FailNow()
- }
+ migrateSchema(t, c, o)
return c
}
@@ -69,9 +68,17 @@ func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Cl
func NewClient(t TestingT, opts ...Option) *ent.Client {
o := newOptions(opts)
c := ent.NewClient(o.opts...)
- if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
+ migrateSchema(t, c, o)
+ return c
+}
+func migrateSchema(t TestingT, c *ent.Client, o *options) {
+ tables, err := schema.CopyTables(migrate.Tables)
+ if err != nil {
+ t.Error(err)
+ t.FailNow()
+ }
+ if err := migrate.Create(context.Background(), c.Schema, tables, o.migrateOpts...); err != nil {
t.Error(err)
t.FailNow()
}
- return c
}
diff --git a/ent/generate.go b/ent/generate.go
index 8d3fdfd..8232761 100644
--- a/ent/generate.go
+++ b/ent/generate.go
@@ -1,3 +1,3 @@
package ent
-//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
+//go:generate go run -mod=mod entc.go
diff --git a/ent/hook/hook.go b/ent/hook/hook.go
index 00aec9a..68bc532 100644
--- a/ent/hook/hook.go
+++ b/ent/hook/hook.go
@@ -1,11 +1,12 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package hook
import (
"context"
"fmt"
- "goweb/ent"
+
+ "github.com/camzawacki/personal-site/ent"
)
// The PasswordTokenFunc type is an adapter to allow the use of ordinary
@@ -14,11 +15,10 @@ type PasswordTokenFunc func(context.Context, *ent.PasswordTokenMutation) (ent.Va
// Mutate calls f(ctx, m).
func (f PasswordTokenFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
- mv, ok := m.(*ent.PasswordTokenMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.PasswordTokenMutation", m)
+ if mv, ok := m.(*ent.PasswordTokenMutation); ok {
+ return f(ctx, mv)
}
- return f(ctx, mv)
+ return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.PasswordTokenMutation", m)
}
// The UserFunc type is an adapter to allow the use of ordinary
@@ -27,11 +27,10 @@ type UserFunc func(context.Context, *ent.UserMutation) (ent.Value, error)
// Mutate calls f(ctx, m).
func (f UserFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
- mv, ok := m.(*ent.UserMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.UserMutation", m)
+ if mv, ok := m.(*ent.UserMutation); ok {
+ return f(ctx, mv)
}
- return f(ctx, mv)
+ return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.UserMutation", m)
}
// Condition is a hook condition function.
@@ -129,7 +128,6 @@ func HasFields(field string, fields ...string) Condition {
// If executes the given hook under condition.
//
// hook.If(ComputeAverage, And(HasFields(...), HasAddedFields(...)))
-//
func If(hk ent.Hook, cond Condition) ent.Hook {
return func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
@@ -144,7 +142,6 @@ func If(hk ent.Hook, cond Condition) ent.Hook {
// On executes the given hook only for the given operation.
//
// hook.On(Log, ent.Delete|ent.Create)
-//
func On(hk ent.Hook, op ent.Op) ent.Hook {
return If(hk, HasOp(op))
}
@@ -152,7 +149,6 @@ func On(hk ent.Hook, op ent.Op) ent.Hook {
// Unless skips the given hook only for the given operation.
//
// hook.Unless(Log, ent.Update|ent.UpdateOne)
-//
func Unless(hk ent.Hook, op ent.Op) ent.Hook {
return If(hk, Not(HasOp(op)))
}
@@ -173,7 +169,6 @@ func FixedError(err error) ent.Hook {
// Reject(ent.Delete|ent.Update),
// }
// }
-//
func Reject(op ent.Op) ent.Hook {
hk := FixedError(fmt.Errorf("%s operation is not allowed", op))
return On(hk, op)
diff --git a/ent/migrate/migrate.go b/ent/migrate/migrate.go
index e4a9a22..1956a6b 100644
--- a/ent/migrate/migrate.go
+++ b/ent/migrate/migrate.go
@@ -1,4 +1,4 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package migrate
@@ -28,17 +28,13 @@ var (
// and therefore, it's recommended to enable this option to get more
// flexibility in the schema changes.
WithDropIndex = schema.WithDropIndex
- // WithFixture sets the foreign-key renaming option to the migration when upgrading
- // ent from v0.1.0 (issue-#285). Defaults to false.
- WithFixture = schema.WithFixture
// WithForeignKeys enables creating foreign-key in schema DDL. This defaults to true.
WithForeignKeys = schema.WithForeignKeys
)
// Schema is the API for creating, migrating and dropping a schema.
type Schema struct {
- drv dialect.Driver
- universalID bool
+ drv dialect.Driver
}
// NewSchema creates a new schema client.
@@ -46,27 +42,23 @@ func NewSchema(drv dialect.Driver) *Schema { return &Schema{drv: drv} }
// Create creates all schema resources.
func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error {
+ return Create(ctx, s, Tables, opts...)
+}
+
+// Create creates all table resources using the given schema driver.
+func Create(ctx context.Context, s *Schema, tables []*schema.Table, opts ...schema.MigrateOption) error {
migrate, err := schema.NewMigrate(s.drv, opts...)
if err != nil {
return fmt.Errorf("ent/migrate: %w", err)
}
- return migrate.Create(ctx, Tables...)
+ return migrate.Create(ctx, tables...)
}
// WriteTo writes the schema changes to w instead of running them against the database.
//
-// if err := client.Schema.WriteTo(context.Background(), os.Stdout); err != nil {
+// if err := client.Schema.WriteTo(context.Background(), os.Stdout); err != nil {
// log.Fatal(err)
-// }
-//
+// }
func (s *Schema) WriteTo(ctx context.Context, w io.Writer, opts ...schema.MigrateOption) error {
- drv := &schema.WriteDriver{
- Writer: w,
- Driver: s.drv,
- }
- migrate, err := schema.NewMigrate(drv, opts...)
- if err != nil {
- return fmt.Errorf("ent/migrate: %w", err)
- }
- return migrate.Create(ctx, Tables...)
+ return Create(ctx, &Schema{drv: &schema.WriteDriver{Writer: w, Driver: s.drv}}, Tables, opts...)
}
diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go
index 005da13..07dd0c7 100644
--- a/ent/migrate/schema.go
+++ b/ent/migrate/schema.go
@@ -1,4 +1,4 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package migrate
@@ -11,9 +11,9 @@ var (
// PasswordTokensColumns holds the columns for the "password_tokens" table.
PasswordTokensColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt, Increment: true},
- {Name: "hash", Type: field.TypeString},
+ {Name: "token", Type: field.TypeString},
{Name: "created_at", Type: field.TypeTime},
- {Name: "password_token_user", Type: field.TypeInt, Nullable: true},
+ {Name: "user_id", Type: field.TypeInt},
}
// PasswordTokensTable holds the schema information for the "password_tokens" table.
PasswordTokensTable = &schema.Table{
@@ -25,7 +25,7 @@ var (
Symbol: "password_tokens_users_user",
Columns: []*schema.Column{PasswordTokensColumns[3]},
RefColumns: []*schema.Column{UsersColumns[0]},
- OnDelete: schema.SetNull,
+ OnDelete: schema.NoAction,
},
},
}
@@ -35,6 +35,8 @@ var (
{Name: "name", Type: field.TypeString},
{Name: "email", Type: field.TypeString, Unique: true},
{Name: "password", Type: field.TypeString},
+ {Name: "verified", Type: field.TypeBool, Default: false},
+ {Name: "admin", Type: field.TypeBool, Default: false},
{Name: "created_at", Type: field.TypeTime},
}
// UsersTable holds the schema information for the "users" table.
diff --git a/ent/mutation.go b/ent/mutation.go
index bc344c1..8128be7 100644
--- a/ent/mutation.go
+++ b/ent/mutation.go
@@ -1,17 +1,19 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
+ "errors"
"fmt"
- "goweb/ent/passwordtoken"
- "goweb/ent/predicate"
- "goweb/ent/user"
"sync"
"time"
"entgo.io/ent"
+ "entgo.io/ent/dialect/sql"
+ "github.com/camzawacki/personal-site/ent/passwordtoken"
+ "github.com/camzawacki/personal-site/ent/predicate"
+ "github.com/camzawacki/personal-site/ent/user"
)
const (
@@ -33,7 +35,7 @@ type PasswordTokenMutation struct {
op Op
typ string
id *int
- hash *string
+ token *string
created_at *time.Time
clearedFields map[string]struct{}
user *int
@@ -73,7 +75,7 @@ func withPasswordTokenID(id int) passwordtokenOption {
m.oldValue = func(ctx context.Context) (*PasswordToken, error) {
once.Do(func() {
if m.done {
- err = fmt.Errorf("querying old values post mutation is not allowed")
+ err = errors.New("querying old values post mutation is not allowed")
} else {
value, err = m.Client().PasswordToken.Get(ctx, id)
}
@@ -106,7 +108,7 @@ func (m PasswordTokenMutation) Client() *Client {
// it returns an error otherwise.
func (m PasswordTokenMutation) Tx() (*Tx, error) {
if _, ok := m.driver.(*txDriver); !ok {
- return nil, fmt.Errorf("ent: mutation is not running in a transaction")
+ return nil, errors.New("ent: mutation is not running in a transaction")
}
tx := &Tx{config: m.config}
tx.init()
@@ -122,40 +124,95 @@ func (m *PasswordTokenMutation) ID() (id int, exists bool) {
return *m.id, true
}
-// SetHash sets the "hash" field.
-func (m *PasswordTokenMutation) SetHash(s string) {
- m.hash = &s
+// IDs queries the database and returns the entity ids that match the mutation's predicate.
+// That means, if the mutation is applied within a transaction with an isolation level such
+// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated
+// or updated by the mutation.
+func (m *PasswordTokenMutation) IDs(ctx context.Context) ([]int, error) {
+ switch {
+ case m.op.Is(OpUpdateOne | OpDeleteOne):
+ id, exists := m.ID()
+ if exists {
+ return []int{id}, nil
+ }
+ fallthrough
+ case m.op.Is(OpUpdate | OpDelete):
+ return m.Client().PasswordToken.Query().Where(m.predicates...).IDs(ctx)
+ default:
+ return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op)
+ }
}
-// Hash returns the value of the "hash" field in the mutation.
-func (m *PasswordTokenMutation) Hash() (r string, exists bool) {
- v := m.hash
+// SetToken sets the "token" field.
+func (m *PasswordTokenMutation) SetToken(s string) {
+ m.token = &s
+}
+
+// Token returns the value of the "token" field in the mutation.
+func (m *PasswordTokenMutation) Token() (r string, exists bool) {
+ v := m.token
if v == nil {
return
}
return *v, true
}
-// OldHash returns the old "hash" field's value of the PasswordToken entity.
+// OldToken returns the old "token" field's value of the PasswordToken entity.
// If the PasswordToken object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
-func (m *PasswordTokenMutation) OldHash(ctx context.Context) (v string, err error) {
+func (m *PasswordTokenMutation) OldToken(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
- return v, fmt.Errorf("OldHash is only allowed on UpdateOne operations")
+ return v, errors.New("OldToken is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
- return v, fmt.Errorf("OldHash requires an ID field in the mutation")
+ return v, errors.New("OldToken requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
- return v, fmt.Errorf("querying old value for OldHash: %w", err)
+ return v, fmt.Errorf("querying old value for OldToken: %w", err)
}
- return oldValue.Hash, nil
+ return oldValue.Token, nil
}
-// ResetHash resets all changes to the "hash" field.
-func (m *PasswordTokenMutation) ResetHash() {
- m.hash = nil
+// ResetToken resets all changes to the "token" field.
+func (m *PasswordTokenMutation) ResetToken() {
+ m.token = nil
+}
+
+// SetUserID sets the "user_id" field.
+func (m *PasswordTokenMutation) SetUserID(i int) {
+ m.user = &i
+}
+
+// UserID returns the value of the "user_id" field in the mutation.
+func (m *PasswordTokenMutation) UserID() (r int, exists bool) {
+ v := m.user
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldUserID returns the old "user_id" field's value of the PasswordToken entity.
+// If the PasswordToken object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *PasswordTokenMutation) OldUserID(ctx context.Context) (v int, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldUserID is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldUserID requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldUserID: %w", err)
+ }
+ return oldValue.UserID, nil
+}
+
+// ResetUserID resets all changes to the "user_id" field.
+func (m *PasswordTokenMutation) ResetUserID() {
+ m.user = nil
}
// SetCreatedAt sets the "created_at" field.
@@ -177,10 +234,10 @@ func (m *PasswordTokenMutation) CreatedAt() (r time.Time, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *PasswordTokenMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) {
if !m.op.Is(OpUpdateOne) {
- return v, fmt.Errorf("OldCreatedAt is only allowed on UpdateOne operations")
+ return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
- return v, fmt.Errorf("OldCreatedAt requires an ID field in the mutation")
+ return v, errors.New("OldCreatedAt requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@@ -194,14 +251,10 @@ func (m *PasswordTokenMutation) ResetCreatedAt() {
m.created_at = nil
}
-// SetUserID sets the "user" edge to the User entity by id.
-func (m *PasswordTokenMutation) SetUserID(id int) {
- m.user = &id
-}
-
// ClearUser clears the "user" edge to the User entity.
func (m *PasswordTokenMutation) ClearUser() {
m.cleareduser = true
+ m.clearedFields[passwordtoken.FieldUserID] = struct{}{}
}
// UserCleared reports if the "user" edge to the User entity was cleared.
@@ -209,14 +262,6 @@ func (m *PasswordTokenMutation) UserCleared() bool {
return m.cleareduser
}
-// UserID returns the "user" edge ID in the mutation.
-func (m *PasswordTokenMutation) UserID() (id int, exists bool) {
- if m.user != nil {
- return *m.user, true
- }
- return
-}
-
// UserIDs returns the "user" edge IDs in the mutation.
// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use
// UserID instead. It exists only for internal usage by the builders.
@@ -238,11 +283,26 @@ func (m *PasswordTokenMutation) Where(ps ...predicate.PasswordToken) {
m.predicates = append(m.predicates, ps...)
}
+// WhereP appends storage-level predicates to the PasswordTokenMutation builder. Using this method,
+// users can use type-assertion to append predicates that do not depend on any generated package.
+func (m *PasswordTokenMutation) WhereP(ps ...func(*sql.Selector)) {
+ p := make([]predicate.PasswordToken, len(ps))
+ for i := range ps {
+ p[i] = ps[i]
+ }
+ m.Where(p...)
+}
+
// Op returns the operation name.
func (m *PasswordTokenMutation) Op() Op {
return m.op
}
+// SetOp allows setting the mutation operation.
+func (m *PasswordTokenMutation) SetOp(op Op) {
+ m.op = op
+}
+
// Type returns the node type of this mutation (PasswordToken).
func (m *PasswordTokenMutation) Type() string {
return m.typ
@@ -252,9 +312,12 @@ func (m *PasswordTokenMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *PasswordTokenMutation) Fields() []string {
- fields := make([]string, 0, 2)
- if m.hash != nil {
- fields = append(fields, passwordtoken.FieldHash)
+ fields := make([]string, 0, 3)
+ if m.token != nil {
+ fields = append(fields, passwordtoken.FieldToken)
+ }
+ if m.user != nil {
+ fields = append(fields, passwordtoken.FieldUserID)
}
if m.created_at != nil {
fields = append(fields, passwordtoken.FieldCreatedAt)
@@ -267,8 +330,10 @@ func (m *PasswordTokenMutation) Fields() []string {
// schema.
func (m *PasswordTokenMutation) Field(name string) (ent.Value, bool) {
switch name {
- case passwordtoken.FieldHash:
- return m.Hash()
+ case passwordtoken.FieldToken:
+ return m.Token()
+ case passwordtoken.FieldUserID:
+ return m.UserID()
case passwordtoken.FieldCreatedAt:
return m.CreatedAt()
}
@@ -280,8 +345,10 @@ func (m *PasswordTokenMutation) Field(name string) (ent.Value, bool) {
// database failed.
func (m *PasswordTokenMutation) OldField(ctx context.Context, name string) (ent.Value, error) {
switch name {
- case passwordtoken.FieldHash:
- return m.OldHash(ctx)
+ case passwordtoken.FieldToken:
+ return m.OldToken(ctx)
+ case passwordtoken.FieldUserID:
+ return m.OldUserID(ctx)
case passwordtoken.FieldCreatedAt:
return m.OldCreatedAt(ctx)
}
@@ -293,12 +360,19 @@ func (m *PasswordTokenMutation) OldField(ctx context.Context, name string) (ent.
// type.
func (m *PasswordTokenMutation) SetField(name string, value ent.Value) error {
switch name {
- case passwordtoken.FieldHash:
+ case passwordtoken.FieldToken:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
- m.SetHash(v)
+ m.SetToken(v)
+ return nil
+ case passwordtoken.FieldUserID:
+ v, ok := value.(int)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetUserID(v)
return nil
case passwordtoken.FieldCreatedAt:
v, ok := value.(time.Time)
@@ -314,13 +388,16 @@ func (m *PasswordTokenMutation) SetField(name string, value ent.Value) error {
// AddedFields returns all numeric fields that were incremented/decremented during
// this mutation.
func (m *PasswordTokenMutation) AddedFields() []string {
- return nil
+ var fields []string
+ return fields
}
// AddedField returns the numeric value that was incremented/decremented on a field
// with the given name. The second boolean return value indicates that this field
// was not set, or was not defined in the schema.
func (m *PasswordTokenMutation) AddedField(name string) (ent.Value, bool) {
+ switch name {
+ }
return nil, false
}
@@ -356,8 +433,11 @@ func (m *PasswordTokenMutation) ClearField(name string) error {
// It returns an error if the field is not defined in the schema.
func (m *PasswordTokenMutation) ResetField(name string) error {
switch name {
- case passwordtoken.FieldHash:
- m.ResetHash()
+ case passwordtoken.FieldToken:
+ m.ResetToken()
+ return nil
+ case passwordtoken.FieldUserID:
+ m.ResetUserID()
return nil
case passwordtoken.FieldCreatedAt:
m.ResetCreatedAt()
@@ -396,8 +476,6 @@ func (m *PasswordTokenMutation) RemovedEdges() []string {
// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with
// the given name in this mutation.
func (m *PasswordTokenMutation) RemovedIDs(name string) []ent.Value {
- switch name {
- }
return nil
}
@@ -451,6 +529,8 @@ type UserMutation struct {
name *string
email *string
password *string
+ verified *bool
+ admin *bool
created_at *time.Time
clearedFields map[string]struct{}
owner map[int]struct{}
@@ -491,7 +571,7 @@ func withUserID(id int) userOption {
m.oldValue = func(ctx context.Context) (*User, error) {
once.Do(func() {
if m.done {
- err = fmt.Errorf("querying old values post mutation is not allowed")
+ err = errors.New("querying old values post mutation is not allowed")
} else {
value, err = m.Client().User.Get(ctx, id)
}
@@ -524,7 +604,7 @@ func (m UserMutation) Client() *Client {
// it returns an error otherwise.
func (m UserMutation) Tx() (*Tx, error) {
if _, ok := m.driver.(*txDriver); !ok {
- return nil, fmt.Errorf("ent: mutation is not running in a transaction")
+ return nil, errors.New("ent: mutation is not running in a transaction")
}
tx := &Tx{config: m.config}
tx.init()
@@ -540,6 +620,25 @@ func (m *UserMutation) ID() (id int, exists bool) {
return *m.id, true
}
+// IDs queries the database and returns the entity ids that match the mutation's predicate.
+// That means, if the mutation is applied within a transaction with an isolation level such
+// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated
+// or updated by the mutation.
+func (m *UserMutation) IDs(ctx context.Context) ([]int, error) {
+ switch {
+ case m.op.Is(OpUpdateOne | OpDeleteOne):
+ id, exists := m.ID()
+ if exists {
+ return []int{id}, nil
+ }
+ fallthrough
+ case m.op.Is(OpUpdate | OpDelete):
+ return m.Client().User.Query().Where(m.predicates...).IDs(ctx)
+ default:
+ return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op)
+ }
+}
+
// SetName sets the "name" field.
func (m *UserMutation) SetName(s string) {
m.name = &s
@@ -559,10 +658,10 @@ func (m *UserMutation) Name() (r string, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldName(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
- return v, fmt.Errorf("OldName is only allowed on UpdateOne operations")
+ return v, errors.New("OldName is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
- return v, fmt.Errorf("OldName requires an ID field in the mutation")
+ return v, errors.New("OldName requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@@ -595,10 +694,10 @@ func (m *UserMutation) Email() (r string, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldEmail(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
- return v, fmt.Errorf("OldEmail is only allowed on UpdateOne operations")
+ return v, errors.New("OldEmail is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
- return v, fmt.Errorf("OldEmail requires an ID field in the mutation")
+ return v, errors.New("OldEmail requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@@ -631,10 +730,10 @@ func (m *UserMutation) Password() (r string, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldPassword(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
- return v, fmt.Errorf("OldPassword is only allowed on UpdateOne operations")
+ return v, errors.New("OldPassword is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
- return v, fmt.Errorf("OldPassword requires an ID field in the mutation")
+ return v, errors.New("OldPassword requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@@ -648,6 +747,78 @@ func (m *UserMutation) ResetPassword() {
m.password = nil
}
+// SetVerified sets the "verified" field.
+func (m *UserMutation) SetVerified(b bool) {
+ m.verified = &b
+}
+
+// Verified returns the value of the "verified" field in the mutation.
+func (m *UserMutation) Verified() (r bool, exists bool) {
+ v := m.verified
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldVerified returns the old "verified" field's value of the User entity.
+// If the User object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *UserMutation) OldVerified(ctx context.Context) (v bool, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldVerified is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldVerified requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldVerified: %w", err)
+ }
+ return oldValue.Verified, nil
+}
+
+// ResetVerified resets all changes to the "verified" field.
+func (m *UserMutation) ResetVerified() {
+ m.verified = nil
+}
+
+// SetAdmin sets the "admin" field.
+func (m *UserMutation) SetAdmin(b bool) {
+ m.admin = &b
+}
+
+// Admin returns the value of the "admin" field in the mutation.
+func (m *UserMutation) Admin() (r bool, exists bool) {
+ v := m.admin
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldAdmin returns the old "admin" field's value of the User entity.
+// If the User object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *UserMutation) OldAdmin(ctx context.Context) (v bool, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldAdmin is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldAdmin requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldAdmin: %w", err)
+ }
+ return oldValue.Admin, nil
+}
+
+// ResetAdmin resets all changes to the "admin" field.
+func (m *UserMutation) ResetAdmin() {
+ m.admin = nil
+}
+
// SetCreatedAt sets the "created_at" field.
func (m *UserMutation) SetCreatedAt(t time.Time) {
m.created_at = &t
@@ -667,10 +838,10 @@ func (m *UserMutation) CreatedAt() (r time.Time, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) {
if !m.op.Is(OpUpdateOne) {
- return v, fmt.Errorf("OldCreatedAt is only allowed on UpdateOne operations")
+ return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
- return v, fmt.Errorf("OldCreatedAt requires an ID field in the mutation")
+ return v, errors.New("OldCreatedAt requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@@ -743,11 +914,26 @@ func (m *UserMutation) Where(ps ...predicate.User) {
m.predicates = append(m.predicates, ps...)
}
+// WhereP appends storage-level predicates to the UserMutation builder. Using this method,
+// users can use type-assertion to append predicates that do not depend on any generated package.
+func (m *UserMutation) WhereP(ps ...func(*sql.Selector)) {
+ p := make([]predicate.User, len(ps))
+ for i := range ps {
+ p[i] = ps[i]
+ }
+ m.Where(p...)
+}
+
// Op returns the operation name.
func (m *UserMutation) Op() Op {
return m.op
}
+// SetOp allows setting the mutation operation.
+func (m *UserMutation) SetOp(op Op) {
+ m.op = op
+}
+
// Type returns the node type of this mutation (User).
func (m *UserMutation) Type() string {
return m.typ
@@ -757,7 +943,7 @@ func (m *UserMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *UserMutation) Fields() []string {
- fields := make([]string, 0, 4)
+ fields := make([]string, 0, 6)
if m.name != nil {
fields = append(fields, user.FieldName)
}
@@ -767,6 +953,12 @@ func (m *UserMutation) Fields() []string {
if m.password != nil {
fields = append(fields, user.FieldPassword)
}
+ if m.verified != nil {
+ fields = append(fields, user.FieldVerified)
+ }
+ if m.admin != nil {
+ fields = append(fields, user.FieldAdmin)
+ }
if m.created_at != nil {
fields = append(fields, user.FieldCreatedAt)
}
@@ -784,6 +976,10 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) {
return m.Email()
case user.FieldPassword:
return m.Password()
+ case user.FieldVerified:
+ return m.Verified()
+ case user.FieldAdmin:
+ return m.Admin()
case user.FieldCreatedAt:
return m.CreatedAt()
}
@@ -801,6 +997,10 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er
return m.OldEmail(ctx)
case user.FieldPassword:
return m.OldPassword(ctx)
+ case user.FieldVerified:
+ return m.OldVerified(ctx)
+ case user.FieldAdmin:
+ return m.OldAdmin(ctx)
case user.FieldCreatedAt:
return m.OldCreatedAt(ctx)
}
@@ -833,6 +1033,20 @@ func (m *UserMutation) SetField(name string, value ent.Value) error {
}
m.SetPassword(v)
return nil
+ case user.FieldVerified:
+ v, ok := value.(bool)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetVerified(v)
+ return nil
+ case user.FieldAdmin:
+ v, ok := value.(bool)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetAdmin(v)
+ return nil
case user.FieldCreatedAt:
v, ok := value.(time.Time)
if !ok {
@@ -898,6 +1112,12 @@ func (m *UserMutation) ResetField(name string) error {
case user.FieldPassword:
m.ResetPassword()
return nil
+ case user.FieldVerified:
+ m.ResetVerified()
+ return nil
+ case user.FieldAdmin:
+ m.ResetAdmin()
+ return nil
case user.FieldCreatedAt:
m.ResetCreatedAt()
return nil
diff --git a/ent/passwordtoken.go b/ent/passwordtoken.go
index 86f92e9..b84e415 100644
--- a/ent/passwordtoken.go
+++ b/ent/passwordtoken.go
@@ -1,15 +1,16 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
import (
"fmt"
- "goweb/ent/passwordtoken"
- "goweb/ent/user"
"strings"
"time"
+ "entgo.io/ent"
"entgo.io/ent/dialect/sql"
+ "github.com/camzawacki/personal-site/ent/passwordtoken"
+ "github.com/camzawacki/personal-site/ent/user"
)
// PasswordToken is the model entity for the PasswordToken schema.
@@ -17,14 +18,16 @@ type PasswordToken struct {
config `json:"-"`
// ID of the ent.
ID int `json:"id,omitempty"`
- // Hash holds the value of the "hash" field.
- Hash string `json:"-"`
+ // Token holds the value of the "token" field.
+ Token string `json:"-"`
+ // UserID holds the value of the "user_id" field.
+ UserID int `json:"user_id,omitempty"`
// CreatedAt holds the value of the "created_at" field.
CreatedAt time.Time `json:"created_at,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the PasswordTokenQuery when eager-loading is set.
- Edges PasswordTokenEdges `json:"edges"`
- password_token_user *int
+ Edges PasswordTokenEdges `json:"edges"`
+ selectValues sql.SelectValues
}
// PasswordTokenEdges holds the relations/edges for other nodes in the graph.
@@ -39,32 +42,27 @@ type PasswordTokenEdges struct {
// UserOrErr returns the User value or an error if the edge
// was not loaded in eager-loading, or loaded but was not found.
func (e PasswordTokenEdges) UserOrErr() (*User, error) {
- if e.loadedTypes[0] {
- if e.User == nil {
- // The edge user was loaded in eager-loading,
- // but was not found.
- return nil, &NotFoundError{label: user.Label}
- }
+ if e.User != nil {
return e.User, nil
+ } else if e.loadedTypes[0] {
+ return nil, &NotFoundError{label: user.Label}
}
return nil, &NotLoadedError{edge: "user"}
}
// scanValues returns the types for scanning values from sql.Rows.
-func (*PasswordToken) scanValues(columns []string) ([]interface{}, error) {
- values := make([]interface{}, len(columns))
+func (*PasswordToken) scanValues(columns []string) ([]any, error) {
+ values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
- case passwordtoken.FieldID:
+ case passwordtoken.FieldID, passwordtoken.FieldUserID:
values[i] = new(sql.NullInt64)
- case passwordtoken.FieldHash:
+ case passwordtoken.FieldToken:
values[i] = new(sql.NullString)
case passwordtoken.FieldCreatedAt:
values[i] = new(sql.NullTime)
- case passwordtoken.ForeignKeys[0]: // password_token_user
- values[i] = new(sql.NullInt64)
default:
- return nil, fmt.Errorf("unexpected column %q for type PasswordToken", columns[i])
+ values[i] = new(sql.UnknownType)
}
}
return values, nil
@@ -72,7 +70,7 @@ func (*PasswordToken) scanValues(columns []string) ([]interface{}, error) {
// assignValues assigns the values that were returned from sql.Rows (after scanning)
// to the PasswordToken fields.
-func (pt *PasswordToken) assignValues(columns []string, values []interface{}) error {
+func (_m *PasswordToken) assignValues(columns []string, values []any) error {
if m, n := len(values), len(columns); m < n {
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
}
@@ -83,71 +81,76 @@ func (pt *PasswordToken) assignValues(columns []string, values []interface{}) er
if !ok {
return fmt.Errorf("unexpected type %T for field id", value)
}
- pt.ID = int(value.Int64)
- case passwordtoken.FieldHash:
+ _m.ID = int(value.Int64)
+ case passwordtoken.FieldToken:
if value, ok := values[i].(*sql.NullString); !ok {
- return fmt.Errorf("unexpected type %T for field hash", values[i])
+ return fmt.Errorf("unexpected type %T for field token", values[i])
} else if value.Valid {
- pt.Hash = value.String
+ _m.Token = value.String
+ }
+ case passwordtoken.FieldUserID:
+ if value, ok := values[i].(*sql.NullInt64); !ok {
+ return fmt.Errorf("unexpected type %T for field user_id", values[i])
+ } else if value.Valid {
+ _m.UserID = int(value.Int64)
}
case passwordtoken.FieldCreatedAt:
if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field created_at", values[i])
} else if value.Valid {
- pt.CreatedAt = value.Time
- }
- case passwordtoken.ForeignKeys[0]:
- if value, ok := values[i].(*sql.NullInt64); !ok {
- return fmt.Errorf("unexpected type %T for edge-field password_token_user", value)
- } else if value.Valid {
- pt.password_token_user = new(int)
- *pt.password_token_user = int(value.Int64)
+ _m.CreatedAt = value.Time
}
+ default:
+ _m.selectValues.Set(columns[i], values[i])
}
}
return nil
}
+// Value returns the ent.Value that was dynamically selected and assigned to the PasswordToken.
+// This includes values selected through modifiers, order, etc.
+func (_m *PasswordToken) Value(name string) (ent.Value, error) {
+ return _m.selectValues.Get(name)
+}
+
// QueryUser queries the "user" edge of the PasswordToken entity.
-func (pt *PasswordToken) QueryUser() *UserQuery {
- return (&PasswordTokenClient{config: pt.config}).QueryUser(pt)
+func (_m *PasswordToken) QueryUser() *UserQuery {
+ return NewPasswordTokenClient(_m.config).QueryUser(_m)
}
// Update returns a builder for updating this PasswordToken.
// Note that you need to call PasswordToken.Unwrap() before calling this method if this PasswordToken
// was returned from a transaction, and the transaction was committed or rolled back.
-func (pt *PasswordToken) Update() *PasswordTokenUpdateOne {
- return (&PasswordTokenClient{config: pt.config}).UpdateOne(pt)
+func (_m *PasswordToken) Update() *PasswordTokenUpdateOne {
+ return NewPasswordTokenClient(_m.config).UpdateOne(_m)
}
// Unwrap unwraps the PasswordToken entity that was returned from a transaction after it was closed,
// so that all future queries will be executed through the driver which created the transaction.
-func (pt *PasswordToken) Unwrap() *PasswordToken {
- tx, ok := pt.config.driver.(*txDriver)
+func (_m *PasswordToken) Unwrap() *PasswordToken {
+ _tx, ok := _m.config.driver.(*txDriver)
if !ok {
panic("ent: PasswordToken is not a transactional entity")
}
- pt.config.driver = tx.drv
- return pt
+ _m.config.driver = _tx.drv
+ return _m
}
// String implements the fmt.Stringer.
-func (pt *PasswordToken) String() string {
+func (_m *PasswordToken) String() string {
var builder strings.Builder
builder.WriteString("PasswordToken(")
- builder.WriteString(fmt.Sprintf("id=%v", pt.ID))
- builder.WriteString(", hash=hello
hello
`, rec.Body.String()) + }) +} diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go new file mode 100644 index 0000000..fed8e07 --- /dev/null +++ b/pkg/ui/ui.go @@ -0,0 +1,21 @@ +package ui + +import ( + "fmt" + "time" +) + +var ( + // cacheBuster stores the current time as a cache buster for static files. + cacheBuster = fmt.Sprint(time.Now().Unix()) +) + +// PublicFile generates a relative URL to a public file. +func PublicFile(filepath string) string { + return fmt.Sprintf("/%s/%s", "files", filepath) +} + +// StaticFile generates a relative URL to a static file including a cache-buster query parameter. +func StaticFile(filepath string) string { + return fmt.Sprintf("/%s/%s?v=%s", "static", filepath, cacheBuster) +} diff --git a/pkg/ui/ui_test.go b/pkg/ui/ui_test.go new file mode 100644 index 0000000..8cecce8 --- /dev/null +++ b/pkg/ui/ui_test.go @@ -0,0 +1,22 @@ +package ui + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPublicFile(t *testing.T) { + path := "abc.txt" + got := PublicFile(path) + expected := fmt.Sprintf("/%s/%s", "files", path) + assert.Equal(t, expected, got) +} + +func TestStaticFile(t *testing.T) { + path := "abc.txt" + got := StaticFile(path) + expected := fmt.Sprintf("/%s/%s?v=%s", "static", path, cacheBuster) + assert.Equal(t, expected, got) +} diff --git a/public/files/.gitkeep b/public/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/fs.go b/public/fs.go new file mode 100644 index 0000000..63d9025 --- /dev/null +++ b/public/fs.go @@ -0,0 +1,8 @@ +package files + +import ( + "embed" +) + +//go:embed static +var Static embed.FS diff --git a/public/static/favicon.png b/public/static/favicon.png new file mode 100644 index 0000000..ce28c7f Binary files /dev/null and b/public/static/favicon.png differ diff --git a/public/static/gopher.png b/public/static/gopher.png new file mode 100644 index 0000000..24fe5c1 Binary files /dev/null and b/public/static/gopher.png differ diff --git a/public/static/logo.png b/public/static/logo.png new file mode 100644 index 0000000..77d08bf Binary files /dev/null and b/public/static/logo.png differ diff --git a/public/static/main.css b/public/static/main.css new file mode 100644 index 0000000..cec6d1e --- /dev/null +++ b/public/static/main.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.1.10 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-black:#000;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--font-weight-thin:100;--font-weight-semibold:600;--font-weight-bold:700;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dark]:checked),[data-theme=dark]{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@property --radialprogress{syntax: "{{.}}
- {{- end}} -{{end}} \ No newline at end of file diff --git a/templates/components/messages.gohtml b/templates/components/messages.gohtml deleted file mode 100644 index ba0dd5a..0000000 --- a/templates/components/messages.gohtml +++ /dev/null @@ -1,21 +0,0 @@ -{{define "messages"}} - {{- range (.GetMessages "success")}} - {{template "message" dict "Type" "success" "Text" .}} - {{- end}} - {{- range (.GetMessages "info")}} - {{template "message" dict "Type" "info" "Text" .}} - {{- end}} - {{- range (.GetMessages "warning")}} - {{template "message" dict "Type" "warning" "Text" .}} - {{- end}} - {{- range (.GetMessages "danger")}} - {{template "message" dict "Type" "danger" "Text" .}} - {{- end}} -{{end}} - -{{define "message"}} -Frontend
-The following incredible projects make developing advanced, modern frontends possible and simple without having to write a single line of JS or CSS. You can go extremely far without leaving the comfort of Go with server-side rendered HTML.
- {{template "tabs" .Data.FrontendTabs}} - - {{- end}} - - {{- if .Data.BackendTabs}} -Backend
-The following incredible projects provide the foundation of the Go backend. See the repository for a complete list of included projects.
- {{template "tabs" .Data.BackendTabs}} - - {{end}} - - {{- if .Data.ShowCacheWarning}} - - {{- end}} -{{end}} - -{{define "tabs"}} -→ {{.Body}}
Please try again.
- {{else if or (eq .StatusCode 403) (eq .StatusCode 401)}} -You are not authorized to view the requested page.
- {{else if eq .StatusCode 404}} -Click {{link (call .ToURL "home") "here" .Path}} to return home
- {{else}} -Something went wrong
- {{end}} -{{end}} \ No newline at end of file diff --git a/templates/pages/forgot-password.gohtml b/templates/pages/forgot-password.gohtml deleted file mode 100644 index 8a53a0f..0000000 --- a/templates/pages/forgot-password.gohtml +++ /dev/null @@ -1,23 +0,0 @@ -{{define "content"}} - -{{end}} \ No newline at end of file diff --git a/templates/pages/home.gohtml b/templates/pages/home.gohtml deleted file mode 100644 index 9c89fc1..0000000 --- a/templates/pages/home.gohtml +++ /dev/null @@ -1,82 +0,0 @@ -{{define "content"}} - {{- if not (eq .HTMX.Request.Target "posts")}} - {{template "top-content" .}} - {{- end}} - - {{template "posts" .}} - - {{- if not (eq .HTMX.Request.Target "posts")}} - {{template "file-msg" .}} - {{- end}} -{{end}} - -{{define "top-content"}} -
-
-
- {{.Title}}
-
- {{.Body}}
-
- -
- {{- end}} - {{- if not $.Pager.IsEnd}} -- -
- {{- end}} -