diff --git a/.air.toml b/.air.toml
new file mode 100644
index 0000000..c2953af
--- /dev/null
+++ b/.air.toml
@@ -0,0 +1,52 @@
+root = "."
+testdata_dir = "testdata"
+tmp_dir = "tmp"
+
+[build]
+ args_bin = []
+ bin = "./tmp/main"
+ cmd = "make build"
+ delay = 1000
+ exclude_dir = ["assets", "tmp", "vendor", "testdata", "uploads", "dbs", "public"]
+ exclude_file = []
+ exclude_regex = ["_test.go"]
+ exclude_unchanged = false
+ follow_symlink = false
+ full_bin = ""
+ include_dir = []
+ include_ext = ["go", "tpl", "tmpl", "html", "css"]
+ include_file = []
+ kill_delay = "0s"
+ log = "build-errors.log"
+ poll = false
+ poll_interval = 0
+ post_cmd = []
+ pre_cmd = []
+ rerun = false
+ rerun_delay = 500
+ send_interrupt = false
+ stop_on_error = true
+
+[color]
+ app = ""
+ build = "yellow"
+ main = "magenta"
+ runner = "green"
+ watcher = "cyan"
+
+[log]
+ main_only = false
+ silent = false
+ time = false
+
+[misc]
+ clean_on_exit = false
+
+[proxy]
+ app_port = 0
+ enabled = false
+ proxy_port = 0
+
+[screen]
+ clear_on_rebuild = false
+ keep_scroll = true
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..a9b9f6c
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,31 @@
+name: Test
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.23
+
+ - uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cache/go-build
+ ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ - name: Test
+ run: make test
diff --git a/.gitignore b/.gitignore
index 485dee6..b335d69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
.idea
+dbs
+uploads
+tmp
+tailwindcss
+daisyui*
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 2ef4b48..ab97f5e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,52 +1,82 @@
-# Connect to the primary database
-.PHONY: db
-db:
- psql postgresql://admin:admin@localhost:5432/app
+# Get the OS name in lowercase (linux, darwin)
+OS_SYSNAME := $(shell uname -s | tr A-Z a-z)
+# Get the machine architecture (x86_64, arm64)
+OS_MACHINE := $(shell uname -m)
-# Connect to the test database
-.PHONY: db-test
-db-test:
- psql postgresql://admin:admin@localhost:5432/app_test
+# If mac OS, use `macos-arm64` or `macos-x64`
+ifeq ($(OS_SYSNAME),darwin)
+ OS_SYSNAME = macos
+ ifneq ($(OS_MACHINE),arm64)
+ OS_MACHINE = x64
+ endif
+endif
-# Connect to the cache
-.PHONY: cache
-cache:
- redis-cli
+# If Linux, use `linux-x64`
+ifeq ($(OS_SYSNAME),linux)
+ OS_MACHINE = x64
+endif
+
+# The appropriate Tailwind package for your OS will attempt to be automatically determined.
+# If this is not working, hard-code the package you want using these options:
+# https://github.com/tailwindlabs/tailwindcss/releases/latest
+TAILWIND_PACKAGE = tailwindcss-$(OS_SYSNAME)-$(OS_MACHINE)
+
+.PHONY: help
+help: ## Print make targets
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+.PHONY: install
+install: ent-install air-install tailwind-install ## Install all dependencies
+
+.PHONY: tailwind-install
+tailwind-install: ## Install the Tailwind CSS CLI
+ curl -sLo tailwindcss https://github.com/tailwindlabs/tailwindcss/releases/latest/download/$(TAILWIND_PACKAGE)
+ chmod +x tailwindcss
+ curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
+ curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js
-# Install Ent code-generation module
.PHONY: ent-install
-ent-install:
- go get -d entgo.io/ent/cmd/ent
+ent-install: ## Install Ent code-generation module
+ go get entgo.io/ent/cmd/ent
+
+.PHONY: air-install
+air-install: ## Install air
+ go install github.com/air-verse/air@latest
-# Generate Ent code
.PHONY: ent-gen
-ent-gen:
+ent-gen: ## Generate Ent code
go generate ./ent
-# Create a new Ent entity
.PHONY: ent-new
-ent-new:
- go run entgo.io/ent/cmd/ent init $(name)
+ent-new: ## Create a new Ent entity (ie, make ent-new name=MyEntity)
+ go run entgo.io/ent/cmd/ent new $(name)
-# Start the Docker containers
-.PHONY: up
-up:
- docker-compose up -d
- sleep 3
+.PHONY: admin
+admin: ## Create a new admin user (ie, make admin email=myemail@web.com)
+ go run cmd/admin/main.go --email=$(email)
-# Rebuild Docker containers to wipe all data
-.PHONY: reset
-reset:
- docker-compose down
- make up
-
-# Run the application
.PHONY: run
-run:
+run: ## Run the application
clear
- go run main.go
+ go run cmd/web/main.go
+
+.PHONY: watch
+watch: ## Run the application and watch for changes with air to automatically rebuild
+ clear
+ air
-# Run all tests
.PHONY: test
-test:
- go test -p 1 ./...
\ No newline at end of file
+test: ## Run all tests
+ go test ./...
+
+.PHONY: check-updates
+check-updates: ## Check for direct dependency updates
+ go list -u -m -f '{{if not .Indirect}}{{.}}{{end}}' all | grep "\["
+
+.PHONY: css
+css: ## Build and minify Tailwind CSS
+ ./tailwindcss -i tailwind.css -o public/static/main.css -m
+
+.PHONY: build
+build: css ## Build CSS and compile the application binary
+ go build -o ./tmp/main ./cmd/web
diff --git a/README.md b/README.md
index e04d21c..38bcf4e 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,17 @@
-## (NAME) - Rapid, easy full-stack web development starter kit in Go
+## Pagoda: Rapid, easy full-stack web development starter kit in Go
+
+[](https://goreportcard.com/report/github.com/mikestefanello/pagoda)
+[](https://github.com/mikestefanello/pagoda/actions/workflows/test.yml)
+[](https://opensource.org/licenses/MIT)
+[](https://pkg.go.dev/github.com/mikestefanello/pagoda)
+[](https://go.dev)
+[](https://github.com/avelino/awesome-go)
+
+
## Table of Contents
* [Introduction](#introduction)
* [Overview](#overview)
- * [Motivation](#motivation)
* [Foundation](#foundation)
* [Backend](#backend)
* [Frontend](#frontend)
@@ -11,9 +19,11 @@
* [Screenshots](#screenshots)
* [Getting started](#getting-started)
* [Dependencies](#dependencies)
+ * [Getting the code](#getting-the-code)
+ * [Installing tools](#installing-tools)
+ * [Create an admin account](#create-an-admin-account)
* [Start the application](#start-the-application)
- * [Running tests](#running-tests)
- * [Clients](#clients)
+ * [Live reloading](#live-reloading)
* [Service container](#service-container)
* [Dependency injection](#dependency-injection)
* [Test dependencies](#test-dependencies)
@@ -27,77 +37,92 @@
* [Entity types](#entity-types)
* [New entity type](#new-entity-type)
* [Sessions](#sessions)
+ * [Encryption](#encryption)
* [Authentication](#authentication)
* [Login / Logout](#login--logout)
* [Forgot password](#forgot-password)
* [Registration](#registration)
+ * [Admins](#admins)
* [Authenticated user](#authenticated-user)
* [Middleware](#middleware)
+ * [Email verification](#email-verification)
+* [Admin panel](#admin-panel)
+ * [Code generation](#code-generation)
+ * [Access](#access)
+ * [Considerations](#considerations)
+ * [Roadmap](#roadmap)
* [Routes](#routes)
* [Custom middleware](#custom-middleware)
- * [Controller / Dependencies](#controller--dependencies)
- * [Patterns](#patterns)
- * [Custom middleware](#custom-middleware)
+ * [Handlers](#handlers)
+ * [Errors](#errors)
+ * [Redirects](#redirects)
* [Testing](#testing)
* [HTTP server](#http-server)
* [Request / Request helpers](#request--response-helpers)
* [Goquery](#goquery)
-* [Controller](#controller)
- * [Page](#page)
- * [Flash messaging](#flash-messaging)
- * [Pager](#pager)
- * [CSRF](#csrf)
- * [Automatic template parsing](#automatic-template-parsing)
- * [Cached responses](#cached-responses)
- * [Cache tags](#cache-tags)
- * [Cache middleware](#cache-middleware)
- * [Data](#data)
+* [User interface](#user-interface)
+ * [Why Gomponents?](#why-gomponents)
+ * [HTMX support](#htmx-support)
+ * [Header management](#header-management)
+ * [Conditional and partial rendering](#conditional-and-partial-rendering)
+ * [CSRF token](#csrf-token)
+ * [CSS](#css)
+ * [Request](#request)
+ * [Title and metatags](#title-and-metatags)
+ * [URL generation](#url-generation)
+ * [Components](#components)
+ * [Layouts](#layouts)
+ * [Pages](#pages)
+ * [Rendering](#rendering)
* [Forms](#forms)
* [Submission processing](#submission-processing)
* [Inline validation](#inline-validation)
- * [Headers](#headers)
- * [Status code](#status-code)
- * [Metatags](#metatags)
- * [URL and link generation](#url-and-link-generation)
- * [HTMX support](#htmx-support)
- * [Rendering the page](#rendering-the-page)
-* [Template renderer](#template-renderer)
- * [Caching](#caching)
- * [Hot-reload for development](#hot-reload-for-development)
- * [File configuration](#file-configuration)
-* [Funcmap](#funcmap)
+ * [CSRF](#csrf)
+ * [Models](#models)
+ * [Icons](#icons)
+ * [Node caching](#node-caching)
+ * [Flash messaging](#flash-messaging)
+* [Pager](#pager)
* [Cache](#cache)
-* [Static files](#static-files)
+ * [Set data](#set-data)
+ * [Get data](#get-data)
+ * [Flush data](#flush-data)
+ * [Flush tags](#flush-tags)
+* [Tasks](#tasks)
+ * [Queues](#queues)
+ * [Dispatcher](#dispatcher)
+ * [Monitoring tasks and queues](#monitoring-tasks-and-queues)
+* [Cron](#cron)
+* [Files](#files)
+ * [Uploads](#uploads)
+ * [Static files](#static-files)
+ * [Cache-buster](#cache-buster)
+ * [Public files](#public-files)
* [Cache control headers](#cache-control-headers)
- * [Cache-buster](#cache-buster)
* [Email](#email)
* [HTTPS](#https)
* [Logging](#logging)
-* [Roadmap](#roadmap)
* [Credits](#credits)
## Introduction
### Overview
-(NAME) is not a framework but rather a base starter-kit for rapid, easy full-stack web development in Go, aiming to provide much of the functionality you would expect from a complete web framework as well as establishing patterns, procedures and structure for your web application.
-
-todo
+_Pagoda_ is not a framework but rather a base starter-kit for rapid, easy full-stack web development in Go, aiming to provide much of the functionality you would expect from a complete web framework as well as establishing patterns, procedures and structure for your web application.
-### Motivation
+Built on a solid [foundation](#foundation) of well-established frameworks and modules, _Pagoda_ aims to be a starting point for any web application with the benefit over a mega-framework in that you have full control over all of the code, the ability to easily swap any frameworks or modules in or out, no strict patterns or interfaces to follow, and no fear of lock-in.
-It started with [this post](https://news.ycombinator.com/item?id=29311761) on _Hacker News_, asking the community what the _simplest stack to build web apps in 2021_ is. After leaving PHP for Go over a year ago, I didn't have an answer for what I would use if I were to start building a web app tomorrow. If I was still using PHP, _Laravel_ would most likely be the easy answer, but there's nothing quite like that available for Go, especially in terms of adoption and maturity. For good reasons, the community also seems mostly opposed to mega-frameworks.
-
-todo
+While separate JavaScript frontends have surged in popularity, many prefer the reliability, simplicity and speed of a full-stack approach with server-side rendered HTML. Even the popular JS frameworks all have SSR options. This project aims to highlight that _Go_ alone can be powerful and easy to work with as a full-stack solution, and interesting [frontend](#frontend) libraries can provide the same modern functionality and behavior without having to write any JS or CSS at all. In fact, you can even avoid writing HTML as well.
### Foundation
-While many great projects were used to build this, all of which are listed in the _credits_ section, the following provide the foundation of the back and frontend. It's important to note that you are **not required to use any of these**. Swapping any of them out will be relatively easy.
+While many great projects were used to build this, all of which are listed in the [credits](#credits) section, the following provide the foundation of the back and frontend. It's important to note that you are **not required to use any of these**. Swapping any of them out will be relatively easy.
#### Backend
- [Echo](https://echo.labstack.com/): High performance, extensible, minimalist Go web framework.
- [Ent](https://entgo.io/): Simple, yet powerful ORM for modeling and querying data.
+- [Gomponents](https://github.com/maragudk/gomponents): HTML components written in pure Go. They render to HTML 5, and make it easy for you to build reusable components.
#### Frontend
@@ -105,69 +130,103 @@ Go server-side rendered HTML combined with the projects below enable you to crea
- [HTMX](https://htmx.org/): Access AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext.
- [Alpine.js](https://alpinejs.dev/): Rugged, minimal tool for composing behavior directly in your markup. Think of it like jQuery for the modern web. Plop in a script tag and get going.
-- [Bulma](https://bulma.io/): Provides ready-to-use frontend components that you can easily combine to build responsive web interfaces. No JavaScript dependencies.
+- [DaisyUI](https://daisyui.com/): The [Tailwind CSS](https://tailwindcss.com/) plugin you will love! It provides useful component class names to help you write less code and build faster. No JavaScript dependencies.
#### Storage
-- [PostgreSQL](https://www.postgresql.org/): The world's most advanced open source relational database.
-- [Redis](https://redis.io/): In-memory data structure store, used as a database, cache, and message broker.
+- [SQLite](https://sqlite.org/): A small, fast, self-contained, high-reliability, full-featured, SQL database engine and the most used database engine in the world.
+
+Originally, Postgres and Redis were chosen as defaults but since the aim of this project is rapid, simple development, it was changed to SQLite which now provides the primary data storage as well as persistent, background [task queues](#tasks). For [caching](#cache), a simple in-memory solution is provided. If you need to use something like Postgres or Redis, swapping those in can be done quickly and easily. For reference, [this branch](https://github.com/mikestefanello/pagoda/tree/postgres-redis) contains the code that included those (but is no longer maintained).
### Screenshots
-todo
+#### Inline form validation
+
+
+
+#### Switch layout templates, user registration
+
+
+
+#### Alpine.js modal, HTMX AJAX request
+
+
+
+#### User entity list (admin panel)
+
+
+
+#### User entity edit (admin panel)
+
+
+
+#### Monitor task queues (provided by Backlite via the admin panel)
+
+
## Getting started
### Dependencies
-Ensure the following are installed on your system:
+Ensure that [Go](https://go.dev/) is installed on your system.
- - [Go](https://go.dev/)
- - [Docker](https://www.docker.com/)
- - [Docker Compose](https://docs.docker.com/compose/install/)
- - [psql](https://www.postgresql.org/docs/13/app-psql.html) _(optional)_
- - [redis-cli](https://redis.io/topics/rediscli) _(optional)_
+### Getting the code
+
+Start by checking out the repository. Since this repository is a _template_ and not a Go _library_, you **do not** use `go get`.
+
+```
+git clone git@github.com:mikestefanello/pagoda.git
+cd pagoda
+```
+
+### Installing tools
+
+Several optional tools are available to make development easier for you. This includes [Ent](#orm) code-generator, for generating ORM code, [Air](https://github.com/air-verse/air) CLI, to provide [live reloading](#live-reloading), and [Tailwind CSS](https://tailwindcss.com/docs/installation/tailwind-cli) CLI, to generate CSS.
+
+If you don't want to use Tailwind and/or [Daisy UI](https://daisyui.com/), or don't want to use Tailwind's standalone CLI, but rather `npm`, for example, modify the `tailwind-install` and `css` [make targets](https://github.com/mikestefanello/pagoda/blob/main/Makefile) based on your preferences. If the script cannot automatically determine the proper Tailwind package to install, modify the `TAILWIND_PACKAGE` variable to match your operating system.
+
+To easily install all tools, run `make install` from the root of the repo. There are also separate _make targets_ for each tool (run `make help` to list all targets).
+
+### Create an admin account
+
+To access the [admin panel](#admin-panel), you must log in with an admin user and to create your first admin user account, you must use the command-line. Execute `make admin email=your@email.com` from the root of the codebase, and an admin account will be generated using that email address. The console will print the randomly generated password for the account.
+
+Once you have one admin account, you can use that account to manage other users and admins from within the UI.
### Start the application
-After checking out the repository, from within the root, start the Docker containers for the database and cache by executing `make up`.
+From within the root of the codebase, simply run `make run`.
-Once that completes, you can start the application by executing `make run`. By default, you should be able to access the application in your browser at `localhost:8000`.
+By default, you should be able to access the application in your browser at `localhost:8000`. Your data will be stored within the `dbs` directory. If you ever want to quickly delete all data, just remove this directory.
-If you ever want to quickly drop the Docker containers and restart them in order to wipe all data, execute `make reset`.
+These settings, and many others, can be changed via the [configuration](#configuration).
-### Running tests
+### Live reloading
-To run all tests in the application, execute `make test`. This ensures that the tests from each package are not run in parallel. This is required since many packages contain tests that connect to the test database which is dropped and recreated automatically for each package.
+Rather than using `make run`, if you prefer live reloading so your app automatically rebuilds and runs whenever you save code changes, start by installing [Air](https://github.com/air-verse/air), if you haven't already, by running `make air-install` (or `make install` to install all tools), then use `make watch` to start the application with automatic live reloading.
-### Clients
-
-The following _make_ commands are available to make it easy to connect to the database and cache.
-
-- `make db`: Connects to the primary database
-- `make db-test`: Connects to the test database
-- `make cache`: Connects to the cache
+When code changes are detected, `make css` will run to re-compile Tailwind styles automatically before restarting the app. If you choose not to use Tailwind, either modify `make css` to run whatever commands you require, or remove this from the `make build` target.
## Service container
-The container is located at `services/container.go` and is meant to house all of your application's services and/or dependencies. It is easily extensible and can be created and initialized in a single call. The services currently included in the container are:
+The container is located at `pkg/services/container.go` and is meant to house all of your application's services and/or dependencies. It is easily extensible and can be created and initialized in a single call. The services currently included in the container are:
-- Configuration
-- Cache
-- Database
-- ORM
-- Web
-- Validator
- Authentication
+- Cache
+- Configuration
+- Database
+- Files
- Mail
-- Template renderer
+- ORM
+- Tasks
+- Validator
+- Web
-A new container can be created and initialized via `services.NewContainer()`. It can be later shutdown via `Shutdown()`.
+A new container can be created and initialized via `services.NewContainer()`. It can be later shutdown via `Shutdown()`, which will attempt to gracefully shutdown all services.
### Dependency injection
-The container exists to faciliate easy dependency-injection both for services within the container as well as areas of your application that require any of these dependencies. For example, the container is passed to and stored within the `Controller`
- so that the controller and the route using it have full, easy access to all services.
+The container exists to facilitate easy dependency-injection both for services within the container and areas of your application that require any of these dependencies. For example, the container is automatically passed to the `Init()` method of your route [handlers](#handlers) so that the handlers have full, easy access to all services.
### Test dependencies
@@ -175,19 +234,22 @@ It is common that your tests will require access to dependencies, like the datab
## Configuration
-The `config` package provides a flexible, extensible way to store all configuration for the application. Configuration is added to the `Container` as a _Service_, making it accessible across most of the application.
+The `config` package provides a flexible, extensible way to store all configuration for the application. Configuration is added to the `Container` as a _Service_, making it accessible across most of the application.
-Be sure to review and adjust all of the default configuration values provided.
+Be sure to review and adjust all the default configuration values provided in `config/config.yaml`.
### Environment overrides
-Leveraging the functionality of [envdecode](https://github.com/joeshaw/envdecode), all configuration values can be overridden by environment variables. Here is an example of what a configuration value looks like, each of which is a field on a struct:
+Leveraging the functionality of [viper](https://github.com/spf13/viper) to manage configuration, all configuration values can be overridden by environment variables. The name of the variable is determined by the set prefix and the name of the configuration field in `config/config.yaml`.
-```go
-Port uint16 `env:"HTTP_PORT,default=8000"`
+In `config/config.go`, the prefix is set as `pagoda` via `viper.SetEnvPrefix("pagoda")`. Nested fields require an underscore between levels. For example:
+
+```yaml
+http:
+ port: 1234
```
-The value for this field will be set to `8000`, the default, unless the `HTTP_PORT` environment variable is set, in which case the value of the variable will be used. This allows you to easily override configuration values per-environment.
+can be overridden by setting an environment variable with the name `PAGODA_HTTP_PORT`.
### Environments
@@ -197,26 +259,27 @@ A helper function (`config.SwitchEnvironment`) is available to make switching th
```go
func TestMain(m *testing.M) {
- // Set the environment to test
- config.SwitchEnvironment(config.EnvTest)
+ // Set the environment to test
+ config.SwitchEnvironment(config.EnvTest)
- // Start a new container
- c = services.NewContainer()
- defer func() {
- if err := c.Shutdown(); err != nil {
- c.Web.Logger.Fatal(err)
- }
- }()
+ // Start a new container
+ c = services.NewContainer()
- // Run tests
- exitVal := m.Run()
- os.Exit(exitVal)
+ // Run tests
+ exitVal := m.Run()
+
+ // Shutdown the container
+ if err := c.Shutdown(); err != nil {
+ panic(err)
+ }
+
+ os.Exit(exitVal)
}
```
## Database
-The database currently used is [PostgreSQL](https://www.postgresql.org/) but you are free to use whatever you prefer. If you plan to continue using [Ent](https://entgo.io/), the incredible ORM, you can check their supported databases [here](https://entgo.io/docs/dialects). The database-driver and client is provided by [pgx](github.com/jackc/pgx/v4) and included in the `Container`.
+The database currently used is [SQLite](https://sqlite.org/) but you are free to use whatever you prefer. If you plan to continue using [Ent](https://entgo.io/), the incredible ORM, you can check their supported databases [here](https://entgo.io/docs/dialects). The database driver is provided by [go-sqlite3](https://github.com/mattn/go-sqlite3). A reference to the database is included in the `Container` if direct access is required.
Database configuration can be found and managed within the `config` package.
@@ -226,17 +289,19 @@ Database configuration can be found and managed within the `config` package.
### Separate test database
-Since many tests can require a database, this application supports a separate database specifically for tests. Within the `config`, the test database name can be specified at `Config.Database.TestDatabase`.
+Since many tests can require a database, this application supports a separate database specifically for tests. Within the `config`, the test database can be specified at `Config.Database.TestConnection`, which is the database connection string that will be used. By default, this will be an in-memory SQLite database.
-When a `Container` is created, if the [environment](#environments) is set to `config.EnvTest`, the database client will connect to the test database instead, drop the database, recreate it, and run migrations so your tests start with a clean, ready-to-go database. Another benefit is that after the tests execute in a given package, you can connect to the test database to audit the data which can be useful for debugging.
+When a `Container` is created, if the [environment](#environments) is set to `config.EnvTest`, the database client will connect to the test database instead and run migrations so your tests start with a clean, ready-to-go database.
+
+When this project was using Postgres, it would automatically drop and recreate the test database. Since the current default is in-memory, that is no longer needed. If you decide to use a test database not in-memory, you can alter the `Container` initialization code to do this for you.
## ORM
-As previously mentioned, [Ent](https://entgo.io/) is the supplied ORM. It can swapped out, but I highly recommend it. I don't think there is anything comparable for Go, at the current time. If you're not familiar with Ent, take a look through their top-notch [documentation](https://entgo.io/docs/getting-started).
+As previously mentioned, [Ent](https://entgo.io/) is the supplied ORM. It can be swapped out, but I highly recommend it. I don't think there is anything comparable for Go, at the current time. If you decide to remove Ent, you will lose the dynamic [admin panel](#admin-panel) which allows you to administer all entity types from within the UI. If you're not familiar with Ent, take a look through their top-notch [documentation](https://entgo.io/docs/getting-started).
An Ent client is included in the `Container` to provide easy access to the ORM throughout the application.
-Ent relies on code-generation for the entities you create to provide robust, type-safe data operations. Everything within the `ent` package in this repository is generated code for the two entity types listed below with the exception of the schema declaration.
+Ent relies on code-generation for the entities you create to provide robust, type-safe data operations. Everything within the `ent` directory in this repository is generated code for the two entity types listed below except the [schema declaration](#https://github.com/mikestefanello/pagoda/tree/main/ent/schema) and [custom extension](https://github.com/mikestefanello/pagoda/tree/main/ent/admin) to generate code for the [admin panel](#admin-panel).
### Entity types
@@ -258,22 +323,23 @@ The generated code is extremely flexible and impressive. An example to highlight
```go
entity, err := c.ORM.PasswordToken.
Query().
+ Where(passwordtoken.ID(tokenID)).
Where(passwordtoken.HasUserWith(user.ID(userID))).
Where(passwordtoken.CreatedAtGTE(expiration)).
- All(ctx.Request().Context())
+ Only(ctx.Request().Context())
```
-This executes a database query to return all _password token_ entities that belong to a user with a given ID and have a _created at_ timestamp field that is greater than or equal to a given time.
+This executes a database query to return the _password token_ entity with a given ID that belong to a user with a given ID and has a _created at_ timestamp field that is greater than or equal to a given time.
## Sessions
-Sessions are provided and handled via [Gorilla sessions](https://github.com/gorilla/sessions) and configured as middleware in the router located at `routes/router.go`. Session data is currently stored in cookies but there are many [options](https://github.com/gorilla/sessions#store-implementations) available if you wish to use something else.
+Sessions are provided and handled via [Gorilla sessions](https://github.com/gorilla/sessions) and configured as middleware in the router located at `pkg/handlers/router.go`. Session data is currently stored in cookies but there are many [options](https://github.com/gorilla/sessions#store-implementations) available if you wish to use something else.
Here's a simple example of loading data from a session and saving new values:
```go
func SomeFunction(ctx echo.Context) error {
- sess, err := session.Get("some-session-key", ctx)
+ sess, err := session.Get(ctx, "some-session-key")
if err != nil {
return err
}
@@ -283,6 +349,10 @@ func SomeFunction(ctx echo.Context) error {
}
```
+### Encryption
+
+Session data is encrypted for security purposes. The encryption key is stored in [configuration](#configuration) at `Config.App.EncryptionKey`. While the default is fine for local development, it is **imperative** that you change this value for any live environment otherwise session data can be compromised.
+
## Authentication
Included are standard authentication features you expect in any web application. Authentication functionality is bundled as a _Service_ within `services/AuthClient` and added to the `Container`. If you wish to handle authentication in a different manner, you could swap this client out or modify it as needed.
@@ -303,11 +373,11 @@ Users can reset their password in a secure manner by issuing a new password toke
Tokens have a configurable expiration. By default, they expire within 1 hour. This can be controlled in the `config` package. The expiration of the token is not stored in the database, but rather is used only when tokens are loaded for potential usage. This allows you to change the expiration duration and affect existing tokens.
-Since the actual tokens are not stored in the database, the reset URL must contain the user's ID. Using that, `GetValidPasswordToken()` will load all non-expired _password token_ entities belonging to the user, and use `bcrypt` to determine if the token in the URL matches any of the stored hashes.
+Since the actual tokens are not stored in the database, the reset URL must contain the user and password token ID. Using that, `GetValidPasswordToken()` will load a matching, non-expired _password token_ entity belonging to the user, and use `bcrypt` to determine if the token in the URL matches stored hash of the password token entity.
Once a user claims a valid password token, all tokens for that user should be deleted using `DeletePasswordTokens()`.
-Routes are provided to request a password reset email at `user/password` and to reset your password at `user/password/reset/token/:uid/:password_token`.
+Routes are provided to request a password reset email at `user/password` and to reset your password at `user/password/reset/token/:user/:password_token/:token`.
### Registration
@@ -315,6 +385,11 @@ The actual registration of a user is not handled within the `AuthClient` but rat
A route is provided for the user to register at `user/register`.
+
+### Admins
+
+A checkbox field has been added to the `User` entity type to indicate if the user has admin access. If your app requires a more robust authorization system, such as roles and permissions, you could easily replace this field and adjust all usage of it accordingly. If a user has this field checked, they will be able to access the [admin panel](#admin-panel). [Middleware](#middleware) is provided to easily restrict access to routes based on admin status.
+
### Authenticated user
The `AuthClient` has two methods available to get either the `User` entity or the ID of the user currently logged in for a given request. Those methods are `GetAuthenticatedUser()` and `GetAuthenticatedUserID()`.
@@ -325,9 +400,64 @@ Registered for all routes is middleware that will load the currently logged in u
If you wish to require either authentication or non-authentication for a given route, you can use either `middleware.RequireAuthentication()` or `middleware.RequireNoAuthentication()`.
+If you wish to restrict a route to admins only, you can use `middleware.RequireAdmin`.
+
+### Email verification
+
+Most web applications require the user to verify their email address (or other form of contact information). The `User` entity has a field `Verified` to indicate if they have verified themself. When a user successfully registers, an email is sent to them containing a link with a token that will verify their account when visited. This route is currently accessible at `/email/verify/:token` and handled by `pkg/handlers/auth.go`.
+
+There is currently no enforcement that a `User` must be verified in order to access the application. If that is something you desire, it will have to be added in yourself. It was not included because you may want partial access of certain features until the user verifies; or no access at all.
+
+Verification tokens are [JSON Web Tokens](https://jwt.io/) generated and processed by the [jwt](https://github.com/golang-jwt/jwt) module. The tokens are _signed_ using the encryption key stored in [configuration](#configuration) (`Config.App.EncryptionKey`). **It is imperative** that you override this value from the default in any live environments otherwise the data can be comprimised. JWT was chosen because they are secure tokens that do not have to be stored in the database, since the tokens contain all of the data required, including built-in expirations. These were not chosen for password reset tokens because JWT cannot be withdrawn once they are issued which poses a security risk. Since these tokens do not grant access to an account, the ability to withdraw the tokens is not needed.
+
+By default, verification tokens expire 12 hours after they are issued. This can be changed in configuration at `Config.App.EmailVerificationTokenExpiration`. There is currently not a route or form provided to request a new link.
+
+Be sure to review the [email](#email) section since actual email sending is not fully implemented.
+
+To generate a new verification token, the `AuthClient` has a method `GenerateEmailVerificationToken()` which creates a token for a given email address. To verify the token, pass it in to `ValidateEmailVerificationToken()` which will return the email address associated with the token and an error if the token is invalid.
+
+## Admin panel
+
+The admin panel functionality is considered to be in _beta_ and remains under active development, though all features described here are expected to be fully-functional. Please use caution when using these features and be sure to report any issues you encounter.
+
+The _admin panel_ currently includes:
+* A completely dynamic UI to manage all entities defined by _Ent_.
+* A section to monitor all [background tasks and queues](#tasks).
+
+There are no separate templates or interfaces for the admin section (see [screenshots](#screenshots)).
+
+Users with admin [access](#access) will see additional links on the default sidebar at the bottom. As with all default UI components, you can easily move these pages and links to a dedicated section, layout, etc. Clicking on the link for any given entity type will provide a pageable table of entities and the ability to add/edit/delete.
+
+### Code generation
+
+In order to automatically and dynamically provide admin functionality for entities, code generation is used by means of leveraging Ent's [extension API](https://entgo.io/docs/extensions) which makes generating code using the Ent graph schema very easy. A [custom extension](https://github.com/mikestefanello/pagoda/blob/master/ent/admin/extension.go) is provided to generate code that provides flat entity type structs and handler code that work directly with Echo. So, both of those are required in order for any of this to work. Whenever you modify one of your entity types or generate a new one, the admin code will also automatically generate.
+
+Without going in to too much detail here, the generated code provides a [handler](https://github.com/mikestefanello/pagoda/blob/master/ent/admin/handler.go) that is then used by a provided [web handler](https://github.com/mikestefanello/pagoda/blob/master/pkg/handlers/admin.go) to power all the routes used in the admin UI. While the rest of the related code should be simple enough to follow, it's worth calling attention to the highly dynamic [entity form](https://github.com/mikestefanello/pagoda/blob/master/pkg/ui/forms/admin_entity.go) that is constructed using the _Ent_ graph data structure.
+
+### Access
+
+Only admin users can access the admin panel. The details are outlined in the [admins](#admins) and [middleware](#middleware) sections. If you haven't yet generated an admin user, follow [these instructions](#create-an-admin-account).
+
+### Considerations
+
+Since the generated code is completely dynamic, all entity functionality related to creating and editing must be defined within your _Ent_ schema. Refer to the [User](https://github.com/mikestefanello/pagoda/blob/master/ent/schema/user.go) entity schema as an example.
+- Field validation must be defined within each entity field (ie, validating an email address in a _string_ field).
+- Pre-processing must be defined within entity hooks (ie, hashing the user's password).
+- _Sensitive_ fields will be omitted from the UI, and only modified if a value is provided during creation or editing.
+- _Edges_ must be bound to an [edge field](https://entgo.io/docs/schema-edges#edge-field) if you want them visible and editable.
+
+### Roadmap
+
+* Determine which tests should be included and provide them.
+* Inline validation.
+* Either exposed sorting or allow the _handler_ to be configured with sort criteria for each type.
+* Exposed filters.
+* Support all field types (types such as _JSON_ as currently not supported).
+* Control which fields appear in the entity list table.
+
## Routes
-The router functionality is provided by [Echo](https://echo.labstack.com/guide/routing/) and constructed within via the `BuildRouter()` function inside `routes/router.go`. Since the _Echo_ instance is a _Service_ on the `Container` which is passed in to `BuildRouter()`, middleware and routes can be added directly to it.
+The router functionality is provided by [Echo](https://echo.labstack.com/guide/routing/) and constructed within via the `BuildRouter()` function inside `pkg/handlers/router.go`. Since the _Echo_ instance is a _Service_ on the `Container` which is passed in to `BuildRouter()`, middleware and routes can be added directly to it.
### Custom middleware
@@ -335,60 +465,99 @@ By default, a middleware stack is included in the router that makes sense for mo
A `middleware` package is included which you can easily add to along with the custom middleware provided.
-### Controller / Dependencies
+### Handlers
-The `Controller`, which is described in a section below, serves two purposes for routes:
+A `Handler` is a simple type that handles one or more of your routes and allows you to group related routes together (ie, authentication). All provided handlers are located in `pkg/handlers`. _Handlers_ also handle self-registering their routes with the router.
-1) It provides base functionality which can be embedded in each route, most importantly `Page` rendering (described in the `Controller` section below)
-2) It stores a pointer to the `Container`, making all _Services_ available within your route
+#### Example
-While using the `Controller` is not required for your routes, it will certainly make development easier.
+The provided patterns are not required, but were designed to make development as easy as possible.
-See the following section for the proposed pattern.
+For this example, we'll create a new handler which includes a GET and POST route and uses the ORM. Start by creating a file at `pkg/handlers/example.go`.
-### Patterns
-
-These patterns are not required, but were designed to make development as easy as possible.
-
-To declare a new route that will have methods to handle a GET and POST request, for example, start with a new _struct_ type, that embeds the `Controller`:
+1) Define the handler type:
```go
-type Home struct {
- controller.Controller
+type Example struct {
+ orm *ent.Client
+}
+```
+
+2) Register the handler so the router automatically includes it
+
+```go
+func init() {
+ Register(new(Example))
+}
+```
+
+3) Initialize the handler (and inject any required dependencies from the _Container_). This will be called automatically.
+
+```go
+func (e *Example) Init(c *services.Container) error {
+ e.orm = c.ORM
+ return nil
+}
+```
+
+4) Declare the routes
+
+**It is highly recommended** that you provide a `Name` for your routes. Most methods on the back and frontend leverage the route name and parameters in order to generate URLs. All route names are currently stored as consts in the `routenames` package so they are accessible from within the `ui` layer.
+
+```go
+func (e *Example) Routes(g *echo.Group) {
+ g.GET("/example", e.Page).Name = routenames.Example
+ g.POST("/example", c.PageSubmit).Name = routenames.ExampleSubmit
+}
+```
+
+5) Implement your routes
+
+```go
+func (e *Example) Page(ctx echo.Context) error {
+ // add your code here
}
-func (c *Home) Get(ctx echo.Context) error {}
-
-func (c *Home) Post(ctx echo.Context) error {}
+func (e *Example) PageSubmit(ctx echo.Context) error {
+ // add your code here
+}
```
-Then create the route and add to the router:
+### Errors
+
+Routes can return errors to indicate that something wrong happened and an error page should be rendered for the request. Ideally, the error is of type `*echo.HTTPError` to indicate the intended HTTP response code, and optionally a message that will be logged. You can use `return echo.NewHTTPError(http.StatusInternalServerError, "optional message")`, for example. If an error of a different type is returned, an _Internal Server Error_ is assumed.
+
+The [error handler](https://echo.labstack.com/guide/error-handling/) is set to the provided `Handler` in `pkg/handlers/error.go` in the `BuildRouter()` function. That means that if any middleware or route return an error, the request gets routed there. This route passes the status code to the `pages.Error` UI component page, allowing you to easily adjust the markup depending on the error type.
+
+### Redirects
+
+The `pkg/redirect` package makes it easy to perform redirects, especially if you provide names for your routes. The `Redirect` type provides the ability to chain redirect options and also supports automatically handling HTMX redirects for boosted requests.
+
+For example, if your route name is `user_profile` with a URL pattern of `/user/profile/:id`, you can perform a redirect by doing:
```go
-home := Home{Controller: controller.NewController(c)}
-g.GET("/", home.Get).Name = "home"
-g.POST("/", home.Post).Name = "home.post"
+return redirect.New(ctx).
+ Route("user_profile").
+ Params(userID).
+ Query(queryParams).
+ Go()
```
-Your route will now have all methods available on the `Controller` as well as access to the `Container`. It's not required to name the route methods to match the HTTP method.
-
-**It is highly recommended** that you provide a `Name` for your routes. Most methods on the back and frontend leverage the route name and parameters in order to generate URLs.
-
### Testing
Since most of your web application logic will live in your routes, being able to easily test them is important. The following aims to help facilitate that.
-The test setup and helpers reside in `routes/router_test.go`.
+The test setup and helpers reside in `pkg/handlers/router_test.go`.
Only a brief example of route tests were provided in order to highlight what is available. Adding full tests did not seem logical since these routes will most likely be changed or removed in your project.
#### HTTP server
-When the route tests initialize, a new `Container` is created which provides full access to all of the _Services_ that will be available during normal application execution. Also provided is a test HTTP server with the router added. This means your tests can make requests and expect responses exactly as the application would behave outside of tests. You do not need to mock the requests and responses.
+When the route tests initialize, a new `Container` is created which provides full access to all the _Services_ that will be available during normal application execution. Also provided is a test HTTP server with the router added. This means your tests can make requests and expect responses exactly as the application would behave outside of tests. You do not need to mock the requests and responses.
#### Request / Response helpers
-With the test HTTP server setup, test helpers for making HTTP requests and evaluating responses are made available to reduce the amount of code you need to write. See `httpRequest` and `httpResponse` within `routes/router_test.go`.
+With the test HTTP server setup, test helpers for making HTTP requests and evaluating responses are made available to reduce the amount of code you need to write. See `httpRequest` and `httpResponse` within `pkg/handlers/router_test.go`.
Here is an example how to easily make a request and evaluate the response:
@@ -416,44 +585,348 @@ assert.Len(t, h1.Nodes, 1)
assert.Equal(t, "About", h1.Text())
```
-## Controller
+## User interface
-As previously mentioned, the `Controller` acts as a base for your routes, though it is optional. It stores the `Container` which houses all _Services_ (_dependencies_) but also a wide array of functionality aimed at allowing you to build complex responses with ease and consistency.
+### Why Gomponents?
-### Page
+Originally, standard Go templates were chosen for this project and a lot of code was written to build tools to make using them as easy and flexible as possible. That code remains archived in [this branch](https://github.com/mikestefanello/pagoda/tree/templates) but is no longer maintained. Despite providing tools such as a powerful _template renderer_, which did things like automatically compile nested templates to separate layouts from pages, automatically include component templates, support HTMX partial rendering, provide _funcmap_ function helpers, and more, the end result left a lot to be desired. Templates provide no type-safety, child templates are difficult to call when you have multiple arguments, templates are not flexible enough to easily provide reusable components and elements, the _funcmap_ and form submission code often had to return HTML or CSS classes, and more.
-The `Page` is the major building block of your `Controller` responses. It is a _struct_ type located at `controller/page.go`. The concept of the `Page` is that it provides a consistent structure for building responses and transmitting data and functionality to the templates.
+While I was extremely hesitant to adopt a rendering option outside the standard library, if an option exists that I personally feel is far superior, that is what I'm going to go with. [Templ](https://github.com/a-h/templ) was also a consideration as that project has made massive progress, seen an explosion in adoption, and aims to solve all the problems previously mentioned. I did not feel that it was a good fit for this project though as it requires you to know and understand their templating language, to install a CLI and an IDE plugin (which does not work with all IDEs; especially GoLand), and separately compile template code.
-All example routes provided construct and _render_ a `Page`. It's recommended that you review both the `Page` and the example routes as they try to illustrate all included functionality.
+[Gomponents](https://github.com/maragudk/gomponents) allows you to build HTML using nothing except pure, type-safe Go; whether that's entire documents or dynamic, reusable components. [Here](https://www.gomponents.com/) are some basic examples to give you an idea of how it works and [this tool](https://gomponents.morehart.dev/) is incredibly useful for quickly converting HTML to _gomponent_ Go code. When I first came across this library, I was very much against it, and couldn't imagine writing tons of nested function calls just to produce some HTML; especially for complex markup. But after actually spending some time using it to replicate the UI of this project, and feeling the downsides of Go templates, I quickly became a big fan and supporter of this approach. Between this and the chosen JS/CSS libraries, you can literally write your entire frontend without leaving Go.
-As you develop your application, the `Page` can be easily extended to include whatever data or functions you want to provide to your templates.
+Before making any quick judgements of your own, I ask that you deeply consider what you've used in the past, review what previously existed in this project, and compare to the current solution and code presented here. I believe I've laid out the `ui` package in a way that makes building your frontend with _gomponents_ very easy and enjoyable.
-Initializing a new page is simple:
+### HTMX support
+
+[HTMX](https://htmx.org/) is an awesome JavaScript library allows you to access AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext.
+
+Many examples of its usage are available in the included examples:
+- All navigation links use [boost](https://htmx.org/docs/#boosting) which dynamically replaces the page content with an AJAX request, providing a SPA-like experience.
+- All forms use either [boost](https://htmx.org/docs/#boosting) or [hx-post](https://htmx.org/docs/#triggers) to submit via AJAX.
+- The mock search autocomplete modal uses [hx-get](https://htmx.org/docs/#targets) to fetch search results from the server via AJAX and update the UI.
+- The mock posts on the homepage/dashboard use [hx-get](https://htmx.org/docs/#targets) to fetch and page posts via AJAX.
+
+All of this can be easily accomplished without writing any JavaScript at all.
+
+Another benefit of [HTMX](https://htmx.org/) is that it's completely backend-agnostic and does not require any special tools or integrations on the backend, though many things are provided here to make it simple.
+
+#### Header management
+
+Included is an [htmx package](https://github.com/mikestefanello/pagoda/blob/main/pkg/htmx/htmx.go) to read and write [HTTP headers](https://htmx.org/docs/#requests) that HTMX uses to communicate additional information and commands for both the request and response. This allows you, for example, to determine if HTMX is making the given request and what exactly it is doing, which could be useful both in your _route_ and your _ui_.
+
+From within your _route_, you can fetch HTMX request details by calling `htmx.GetRequest(ctx)`, and you can send commands back to HTMX by calling `htmx.Response{...}.Apply(ctx)`, and populating any fields on the `htmx.Response` struct.
+
+From within your _ui_, the [Request](#request) object will automatically contain the request details on the `Htmx` field.
+
+#### Conditional and partial rendering
+
+Since HTMX communicates what it is doing with the server, you can use the request headers to conditionally process in your _route_ or render in your _ui_, if needed.
+
+The most important case to support is _partial_ rendering. If HTMX is making a request, unless it is [boosted](https://htmx.org/docs/#boosting), you only want to render the _content_ of your _route_, and not the entire [layout](#layouts). This is automatically handled by the `Render()` method on the [Request](#request) type. More can be read about that [here](#rendering).
+
+If your routes aren't doing multiple things, you may not need _conditional_ rendering, but it's worth knowing how flexible you can be. A simple example of this:
```go
-func (c *Home) Get(ctx echo.Context) error {
- page := controller.NewPage(ctx)
+if htmx.GetRequest(ctx).Target == "search" {
+ // This request is HTMX fetching content just for the #search element
}
```
-Using the `echo.Context`, the `Page` will be initialized with the following fields populated:
+#### CSRF token
-- `Context`: The passed in _context_
-- `ToURL`: A function the templates can use to generate a URL with a given route name and parameters
-- `Path`: The requested URL path
-- `URL`: The requested URL
-- `StatusCode`: Defaults to 200
-- `Pager`: Initialized `Pager` (see below)
-- `RequestID`: The request ID, if the middleware is being used
+If [CSRF](#csrf) protection is enabled, the token value will automatically be passed to HTMX to be included in all non-GET requests. This is done in the `JS()` [component](#components) by leveraging HTMX [events](https://htmx.org/reference/#events).
+
+### CSS
+
+[DaisyUI](https://daisyui.com/), which is a component library for [Tailwind CSS](https://tailwindcss.com/), was chosen as the default CSS solution. You are not required to use either of these and removing what has been provided should be quite simple. Both of these tools are very mature, have huge communities, and endless resources, making them ideal for rapid, simple frontend development
+
+Review the [installing tools](#installing-tools) section to ensure you have everything installed, and you understand how the `make` commands handle building your CSS styles and how live reloading automatically handles executing the Tailwind CLI. By default, the compiled CSS is written to `public/static/main.css` when `make css` is executed.
+
+Tailwind configuration is stored in [tailwind.css](https://github.com/mikestefanello/pagoda/blob/main/tailwind.css) and is configured to check for classes within `pkg/ui`. Remember, full class names must be present in the Go files in order for Tailwind to find them; you cannot dynamically build classes.
+
+### Request
+
+The `Request` type in the `ui` package is a foundational helper that provides useful data from the current request as well as resources and methods that make rendering UI components much easier. Using the `echo.Context`, a `Request` can be generated by executing `ui.NewRequest(ctx)`. As you develop and expand your application, you will likely want to expand this type to include additional data and methods that your frontend requires.
+
+`NewRequest()` will automatically populate the following fields using the `echo.Context` from the current request:
+
+- `Context`: The provided _echo.Context_
+- `CurrentPath`: The requested URL path
- `IsHome`: If the request was for the homepage
- `IsAuth`: If the user is authenticated
-- `AuthUser`: The logged in user entity, if one
+- `AuthUser`: The logged-in user entity, if one
- `CSRF`: The CSRF token, if the middleware is being used
-- `HTMX.Request`: Data from the HTMX headers, if HTMX made the request (see below)
+- `Htmx`: Data from the HTMX headers, if HTMX made the request
+- `Config`: The application configuration, if the middleware is being used
+
+#### Title and metatags
+
+The `Request` type has additional fields to make it easy to set static values within components being rendered on a given page. While the _title_ is always important, the others are provided as an example:
+
+* `Title`: The page title
+* `Metatags`:
+ * `Description`: The description of the page
+ * `Tags`: A slice of keyword tags
+
+#### URL generation
+
+As mentioned in the [Routes](#routes) section, it is recommended, though not required, to provide names for each of your routes. These are currently defined as consts in the `routenames` package. If you use names for your routes, you can leverage the URL generation methods on the `Request`. This allows you to prevent hard-coding your route paths and parameters in multiple places.
+
+The methods both take a route name and optional variadic route parameters:
+
+* `Path()`: Generates a relative path for a given route.
+* `Url()`: Generates an absolute URL for a given route. This uses the `App.Host` field in your [configuration](#configuration) to determine the host of the URL.
+
+**Example:**
+
+```go
+g.GET("/user/:uid", profilePage).Name = routenames.Profile
+```
+
+```go
+func ProfileLink(r *ui.Request, userName string, userID int64) gomponents.Node {
+ return A(
+ Class("profile"),
+ Href(r.Path(routenames.Profile, userID)),
+ Text(userName),
+ )
+}
+```
+
+### Components
+
+The [components package](https://github.com/mikestefanello/pagoda/tree/templates/pkg/ui/components) is meant to be your library of reusable _gomponent_ components. Having this makes building your [layouts](#layouts), [pages](#pages), [forms](#forms), [models](#models) and the rest of your user interface much easier. Some of the examples provided include components for [flash messages](#flash-messaging), navigation menus, tabs, metatags, and form elements used to automatically provide [inline validation](#inline-form-validation).
+
+Your components can also make using utility-based CSS libraries, such as [Tailwind CSS](https://tailwindcss.com/), much easier by avoiding excessive duplication of classes across elements. A number of [DaisyUI](https://daisyui.com/) components are provided.
+
+Here are some examples:
+
+```go
+Badge(ColorSuccess, "Hello")
+```
+
+```go
+Card(CardParams{
+ Title: "Hello world",
+ Body: Group{
+ Span(Text("This is a card.")),
+ },
+ Footer: Group{
+ ButtonLink(ColorNeutral, "https://daisyui.com", "Learn more"),
+ },
+ Color: ColorInfo,
+ Size: SizeMedium,
+})
+```
+
+```go
+Tabs([]Tab{
+ {
+ Title: "Tab 1",
+ Body: "Here is tab 1.",
+ },
+ {
+ Title: "Tab 2",
+ Body: "Check out tab 2.",
+ },
+})
+```
+
+### Layouts
+
+_Layouts_ are full HTML templates that are used by [pages](#pages) to inject themselves in to, allowing you to easily have multiple pages that all use the same layout, and to easily switch layouts between different pages. [Included](https://github.com/mikestefanello/pagoda/tree/templates/pkg/ui/layouts) is a _primary_ and _auth_ layout as an example, which you can see in action by navigating between the links on the _General_ and _Account_ sidebar menus.
+
+### Pages
+
+_Pages_ are what [route handlers](#handlers) ultimately assemble and render. They may accept primitives, [models](#models), [forms](#forms), or nothing at all, and they embed themselves in a [layout](#layouts) of their choice. Each _page_ represents a different page of your web application and many [examples](https://github.com/mikestefanello/pagoda/tree/templates/pkg/ui/pages) are provided for reference. See below for a minimal example.
+
+#### Rendering
+
+The `Request` type contains a `Render()` method which makes rendering your page within a given layout simple. It automatically handles partial rendering, omitting the [layout](#layouts) and only rendering the [page](#pages) if the request is made by HTMX and is not boosted. Using HTMX is completely optional. This is accomplished by passing in your layout and _page_ separately, for example:
+
+```go
+func MyPage(ctx echo.Context, username string) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "My page"
+ node := Div(Textf("Hello, %s!", username))
+ return r.Render(layouts.Primary, node)
+}
+```
+
+Using `Render()`, in this example, only `node` will render if HTMX made the request in a non-boosted fashion, otherwise `node` will render within `layouts.Primary`.
+
+And from within your [route handler](#handlers), simply:
+
+```go
+func (e *ExampleHandler) Page(ctx echo.Context) error {
+ return pages.MyPage(ctx, "abcd")
+}
+```
+
+### Forms
+
+Building, rendering, validating and processing forms is made extremely easy with [Echo binding](https://echo.labstack.com/guide/binding/), [validator](https://github.com/go-playground/validator), [form.Submission](https://github.com/mikestefanello/pagoda/blob/templates/pkg/form/submission.go), and the provided _gomponent_ [components](#components).
+
+Start by declaring the form within the [forms](https://github.com/mikestefanello/pagoda/tree/templates/pkg/ui/forms) package:
+
+```go
+type Guestbook struct {
+ Message string `form:"message" validate:"required"`
+ form.Submission
+}
+```
+
+Embedding `form.Submission` satisfies the `form.Form` interface and handles submissions and validation for you.
+
+Next, provide a method that renders the form:
+
+```go
+func (f *Guestbook) Render(r *ui.Request) Node {
+ return Form(
+ ID("guestbook"),
+ Method(http.MethodPost),
+ Attr("hx-post", r.Path(routenames.GuestbookSubmit)),
+ TextareaField(TextareaFieldParams{
+ Form: f,
+ FormField: "Message",
+ Name: "message",
+ Label: "Message",
+ Value: f.Message,
+ }),
+ ControlGroup(
+ FormButton(ColorPrimary, "Submit"),
+ ),
+ CSRF(r),
+ )
+}
+```
+
+Then, create a _page_ that includes your form:
+
+```go
+func UserGuestbook(ctx echo.Context, form *forms.Guestbook) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "User page"
+
+ content := Div(
+ Class("guestbook"),
+ H2(Text("My guestbook")),
+ P(Text("Hi, please sign my guestbook!")),
+ form.Render(r)
+ )
+
+ return r.Render(layouts.Primary, content)
+}
+```
+
+And last, have your handler render the _page_ in a route, and provide a route for the submission.
+
+```go
+func (e *Example) Routes(g *echo.Group) {
+ g.GET("/guestbook", e.Page).Name = routenames.Guestbook
+ g.POST("/guestbook", c.PageSubmit).Name = routenames.GuestbookSubmit
+}
+
+func (e *Example) Page(ctx echo.Context) error {
+ return pages.UserGuestbook(ctx, form.Get[forms.Guestbook](ctx))
+}
+```
+
+`form.Get` will either initialize a new form, or load one previously stored in the context (ie, if it was already submitted).
+
+#### Submission processing
+
+Using the example form above, this is all you would have to do within the _POST_ callback for your route:
+
+Start by submitting the form via `form.Submit()`, along with the request context. This will:
+1. Store a pointer to the form in the _context_ so that your _GET_ callback can access the form values (shown previously). That allows the form to easily be re-rendered with any validation errors it may have as well as the values that were provided.
+2. Parse the input in the _POST_ data to map to the struct so the fields becomes populated. This uses the `form` struct tags to map form input values to the struct fields.
+3. Validate the values in the struct fields according to the rules provided in the optional `validate` struct tags.
+
+Then, evaluate the error returned, if one, and process the form values however you need to:
+
+```go
+func (e *Example) Submit(ctx echo.Context) error {
+ var input forms.Guestbook
+
+ // Submit the form.
+ err := form.Submit(ctx, &input)
+
+ // Check the error returned, and act accordingly.
+ switch err.(type) {
+ case nil:
+ // All good!
+ case validator.ValidationErrors:
+ // The form input was not valid, so re-render the form with the errors included.
+ return e.Page(ctx)
+ default:
+ // Request failed, show the error page.
+ return err
+ }
+
+ msg.Success(fmt.Sprintf("Your message was: %s", input.Message))
+
+ return redirect.New(ctx).
+ Route(routenames.Home).
+ Go()
+}
+```
+
+#### Inline validation
+
+The `Submission` makes inline validation easier because it will store all validation errors in a map, keyed by the form struct field name. It also contains helper methods that the provided form [components](#components), such as `TextareaField` shown in the example above, use to automatically provide classes and error messages. The example form above will have inline validation without requiring anything other than what is shown above.
+
+While [validator](https://github.com/go-playground/validator) is a great package that is used to validate based on struct tags, the downside is that the messaging, by default, is not very human-readable or easy to override. Within `Submission.setErrorMessages()` the validation errors are converted to more readable messages based on the tag that failed validation. Only a few tags are provided as an example, so be sure to expand on that as needed.
+
+#### CSRF
+
+By default, all non `GET` requests will require a CSRF token be provided as a form value. This is provided by middleware and can be adjusted or removed in the router.
+
+The `Request` automatically extracts the CSRF token from the context, but you must include it in your forms by using the provided `CSRF()` [component](#components) as shown in the example above.
+
+### Models
+
+Models are objects built and provided by your _routes_ that can be rendered by your _ui_. Though not required, they reside in the [models package](https://github.com/mikestefanello/pagoda/tree/main/pkg/ui/models) and each has a `Render()` method, making them easy to render within your [pages](#pages). Please see example routes such as the homepage and search for an example.
+
+### Icons
+
+A starting SVG icon library is provided in `pkg/ui/icons` with icons from [heroicons](https://heroicons.com/). You are free to use any icons you want and in any manner.
+
+Example:
+```go
+A(
+ Href("/user/123"),
+ icons.UserCircle,
+ Text("Profile")
+)
+```
+
+### Node caching
+
+While most likely unnecessary for most applications, but because optimizing software is fun, a simple `gomponents.Node` cache is provided. This is not because _gomponents_ is inefficient, in fact my basic benchmarks put it as either similar or slightly better than Go templates, but rather because there are _some_ performance gains to be seen by caching static nodes and it may seem wasteful to build and render static HTML on every single page load. It is important to note, you can only cache nodes that are static and will never change.
+
+A good example of this, and one included, is the entire upper navigation bar, search form, and search modal in the _Primary_ layout. It contains a large amount of nested _gomponent_ function calls and a lot of rendering is required. There is no reason to do this more than once.
+
+The cache functions are available in `pkg/ui/cache` and can most easily used like this:
+
+```go
+func SearchModal() gomponents.Node {
+ return cache.SetIfNotExists("searchModal", func() gomponents.Node {
+ return Div(...your entire nested node...)
+ })
+}
+```
+
+`cache.SetIfNotExists()`is a helper function that uses `cache.Get()` to check if the `Node` is already cached under the provided _key_, and if not, executes the _func_ to generate the `Node`, and caches that via `cache.Set()`.
+
+`cache.Set()` does more than just cache the `Node` in-memory. It renders the entire `Node` into a `bytes.Buffer`, then stores a `Raw()` `Node` using the rendered content. This means that everytime the `Node` is taken from the cache and rendered, the pre-rendered `string` is used rather than having to iterate through the nested component, executing all of the element functions and rendering and building the entire HTML output.
+
+It's worth noting that my benchmarking was very limited and cannot be considered anything definitive. In my tests, gomponents was faster, allocated less overall, but had more allocations in total. If you're able to cache static nodes, gomponents can perform significantly better. Reiterating, for most applications, these differences in nanoseconds and bytes will most likely be completely insignificant and unnoticed; but it's worth being aware of.
### Flash messaging
-While flash messaging functionality is provided outside of the `Controller` and `Page`, within the `msg` package, it's really only used within this context.
+Flash messaging functionality is provided within the `msg` package. It is used to provide one-time status messages to users.
Flash messaging requires that [sessions](#sessions) and the session middleware are in place since that is where the messages are stored.
@@ -463,401 +936,218 @@ There are four types of messages, and each can be created as follows:
- Success: `msg.Success(ctx echo.Context, message string)`
- Info: `msg.Info(ctx echo.Context, message string)`
- Warning: `msg.Warning(ctx echo.Context, message string)`
-- Danger: `msg.Danger(ctx echo.Context, message string)`
-
-The _message_ string can contain HTML.
+- Error: `msg.Error(ctx echo.Context, message string)`
#### Rendering messages
-When a flash message is retrieved from storage in order to be rendered, it is deleted from storage so that it cannot be rendered again.
+When a flash message is retrieved from storage to be rendered, it is deleted from storage so that it cannot be rendered again.
-The `Page` has a method that can be used to fetch messages for a given type from within the template: `Page.GetMessages(typ msg.Type)`. This is used rather than the _funcmap_ because the `Page` contains the request context which is required in order to access the session data. Since the `Page` is the data destined for the templates, you can use: `{{.GetMessages "success"}}` for example.
+A [component](#components), `FlashMessages()`, is provided to render flash messages within your UI.
-To make things easier, a template _component_ is already provided, located at `templates/components/messages.gohtml`. This will render all messages of all types simply by using `{{template "messages" .}}` either within your page or layout template.
+## Pager
-### Pager
-
-A very basic mechanism is provided to handle and facilitate paging located in `controller/pager.go`. When a `Page` is initialized, so is a `Pager` at `Page.Pager`. If the requested URL contains a `page` query parameter with a numeric value, that will be set as the page number in the pager.
-
-During initialization, the _items per page_ amount will be set to the default, controlled via constant, which has a value of 20. It can be overridden by changing `Pager.ItemsPerPage` but should be done before other values are set in order to not provide incorrect calculations.
+A very basic mechanism is provided to handle and facilitate paging located in `pkg/pager` and can be initialized via `pager.NewPager()`. If the requested URL contains a `page` query parameter with a numeric value, that will be set as the page number in the pager. This query key can be controlled via the `QueryKey` constant.
Methods include:
- `SetItems(items int)`: Set the total amount of items in the entire result-set
- `IsBeginning()`: Determine if the pager is at the beginning of the pages
- `IsEnd()`: Determine if the pager is at the end of the pages
-- `GetOffset()`: Get the offset which can be useful is constructing a paged database query
+- `GetOffset()`: Get the offset which can be useful in constructing a paged database query
-There is currently no template (yet) to easily render a pager.
-
-### CSRF
-
-By default, all non GET requests will require a CSRF token be provided as a form value. This is provided by middleware and can be adjusted or removed in the router.
-
-The `Page` will contain the CSRF token for the given request. There is a CSRF helper component template which can be used to easily render a hidden form element in your form which will contain the CSRF token and the proper element name. Simply include `{{template "csrf" .}}` within your form.
-
-### Automatic template parsing
-
-Dealing with templates can be quite tedious and annoying so the `Page` aims to make it as simple as possible with the help of the [template renderer](#template-renderer). To start, templates for _pages_ are grouped in the following directories within the `templates` directory:
-
-- `layouts`: Base templates that provide the entire HTML wrapper/layout. This template should include a call to `{{template "content" .}}` to render the content of the `Page`.
-- `pages`: Templates that are specific for a given route/page. These must contain `{{define "content"}}{{end}}` which will be injected in to the _layout_ template.
-- `components`: A shared library of common components that the layout and base template can leverage.
-
-Specifying which templates to render for a given `Page` is as easy as:
-
-```go
-page.Name = "home"
-page.Layout = "main"
-```
-
-That alone will result in the following templates being parsed and executed when the `Page` is rendered:
-
-1) `layouts/main.gohtml` as the base template
-2) `pages/home.gohtml` to provide the `content` template for the layout
-3) All template files located within the `components` directory
-4) The entire [funcmap](#funcmap)
-
-The [template renderer](#template-renderer) also provides caching and local hot-reloading.
-
-### Cached responses
-
-A `Page` can have cached enabled just by setting `Page.Cache.Enabled` to `true`. The `Controller` will automatically handle caching the HTML output, headers and status code. Cached pages are stored using a key that matches the full request URL and [middleware](#cache-middleware) is used to serve it on matching requests.
-
-By default, the cache expiration time will be set according to the configuration value located at `Config.Cache.Expiration.Page` but it can be set per-page at `Page.Cache.Expiration`.
-
-#### Cache tags
-
-You can optionally specify cache tags for the `Page` by setting a slice of strings on `Page.Cache.Tags`. This provides the ability to build in cache invalidation logic in your application driven by events such as entity operations, for example.
-
-The cache client on the `Container` is currently handled by [gocache](https://github.com/eko/gocache) which makes it easy to perform operations such as tag-invalidation, for example:
-
-```go
-c.Cache.Invalidate(ctx, store.InvalidateOptions{
- Tags: []string{"my-tag"},
-})
-```
-
-#### Cache middleware
-
-Cached pages are served via the middleware `ServeCachedPage()` in the `middleware` package.
-
-The cache is bypassed if the requests meet any of the following criteria:
-1) Is not a GET request
-2) Is made by an authenticated user
-
-Cached pages are looked up for a key that matches the exact, full URL of the given request.
-
-### Data
-
-The `Data` field on the `Page` is of type `interface{}` and is what allows your route to pass whatever it requires to the templates, alongside the `Page` itself.
-
-### Forms
-
-The `Form` field on the `Page` is similar to the `Data` field in that it's an `interface{}` type but it's meant to store a struct that represents a form being rendered on the page.
-
-An example of this pattern is:
-
-```go
-type ContactForm struct {
- Email string `form:"email" validate:"required,email"`
- Message string `form:"message" validate:"required"`
- Submission controller.FormSubmission
-}
-```
-
-Then in your page:
-
-```go
-page := controller.NewPage(ctx)
-page.Form = ContactForm{}
-```
-
-How the _form_ gets populated with values so that your template can render them is covered in the next section.
-
-#### Submission processing
-
-Form submission processing is made extremely simple by leveraging functionality provided by [Echo binding](https://echo.labstack.com/guide/binding/), [validator](https://github.com/go-playground/validator) and the `FormSubmission` struct located in `controller/form.go`.
-
-Using the example form above, these are the steps you would take within the _POST_ callback for your route:
-
-Start by storing a pointer to the form in the conetxt so that your _GET_ callback can access the form values, which will be showed at the end:
-```go
-var form ContactForm
-ctx.Set(context.FormKey, &form)
-```
-
-Parse the input in the POST data to map to the struct so it becomes populated. This uses the `form` struct tags to map form values to the struct fields.
-```go
-if err := ctx.Bind(&form); err != nil {
- // Something went wrong...
-}
-```
-
-Process the submission which uses [validator](https://github.com/go-playground/validator) to check for validation errors:
-```go
-if err := form.Submission.Process(ctx, form); err != nil {
- // Something went wrong...
-}
-```
-
-Check if the form submission has any validation errors:
-```go
-if !form.Submission.HasErrors() {
- // All good, now execute something!
-}
-```
-
-In the event of a validation error, you most likely want to re-render the form with the values provided and any error messages. Since you stored a pointer to the _form_ in the context in the first step, you can first have the _POST_ handler call the _GET_:
-```go
-if form.Submission.HasErrors() {
- return c.Get(ctx)
-}
-```
-
-Then, in your _GET_ handler, extract the form from the context so it can be passed to the templates:
-```go
-page := controller.NewPage(ctx)
-page.Form = ContactForm{}
-
-if form := ctx.Get(context.FormKey); form != nil {
- page.Form = form.(*ContactForm)
-}
-```
-
-And finally, your template:
-```
-
-```
-
-#### Inline validation
-
-The `FormSubmission` makes inline validation easier because it will store all validation errors in a map, keyed by the form struct field name. It also contains helper methods that your templates can use to provide classes and extract the error messages.
-
-While [validator](https://github.com/go-playground/validator) is an incredible package that is used to validate based on struct tags, the downside is that the messaging, by default, is not very human-readable or easy to override. Within `FormSubmission.setErrorMessages()` the validation errors are converted to more readable messages based on the tag that failed validation. Only a few tags are provided as an example, so be sure to expand on that as needed.
-
-To provide the inline validation in your template, there are two things that need to be done.
-
-First, include a status class on the element so it will highlight green or red based on the validation:
-```
-
-```
-
-Second, render the error messages, if there are any for a given field:
-```go
-{{template "field-errors" (.Form.Submission.GetFieldErrors "Email")}}
-```
-
-### Headers
-
-HTTP headers can be set either via the `Page` or the _context_:
-
-```go
-page := controller.NewPage(ctx)
-page.Headers["HeaderName"] = "header-value"
-```
-
-```go
-ctx.Response().Header().Set("HeaderName", "header-value")
-```
-
-### Status code
-
-The HTTP response status code can be set either via the `Page` or the _context_:
-
-```go
-page := controller.NewPage(ctx)
-page.StatusCode = http.StatusTooManyRequests
-```
-
-```go
-ctx.Response().Status = http.StatusTooManyRequests
-```
-
-### Metatags
-
-The `Page` provides the ability to set basic HTML metatags which can be especially useful if your web application is publicly accessible. Only fields for the _description_ and _keywords_ are provided but adding additional fields is very easy.
-
-```go
-page := controller.NewPage(ctx)
-page.Metatags.Description = "The page description."
-page.Metatags.Keywords = []string{"Go", "Software"}
-```
-
-A _component_ template is included to render metatags in `core.gohtml` which can be used by adding `{{template "metatags" .}}` to your _layout_.
-
-### URL and link generation
-
-Generating URLs in the templates is made easy if you follow the [routing patterns](#patterns) and provide names for your routes. Echo provides a `Reverse` function to generate a route URL with a given route name and optional parameters. This function is made accessible to the templates via the `Page` field `ToURL`.
-
-As an example, if you have route such as:
-```go
-profile := Profile{Controller: ctr}
-e.GET("/user/profile/:user", profile.Get).Name = "user_profile"
-```
-
-And you want to generate a URL in the template, you can:
-```go
-{{call .ToURL "user_profile" 1}
-```
-
-Which will generate: `/user/profile/1`
-
-There is also a helper function provided in the [funcmap](#funcmap) to generate links which has the benefit of adding an _active_ class if the link URL matches the current path. This is especially useful for navigation menus.
-
-```go
-{{link (call .ToURL "user_profile" .AuthUser.ID) "Profile" .Path "extra-class"}}
-```
-
-Will generate:
-```html
-Profile
-```
-Assuming the current _path_ is `/user/profile/1`; otherwise the `is-active` class will be excluded.
-
-### HTMX support
-
-[HTMX](https://htmx.org/) is an incredible JavaScript library allows you to access AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext.
-
-Many examples of its usage are available in the included examples:
-- All navigation links use [boost](https://htmx.org/docs/#boosting) which dynamically replaces the page content with an AJAX request, providing a SPA-like experience.
-- All forms use either [boost](https://htmx.org/docs/#boosting) or [hx-post](https://htmx.org/docs/#triggers) to submit via AJAX.
-- The mock search autocomplete modal uses [hx-get](https://htmx.org/docs/#targets) to fetch search results from the server via AJAX and update the UI.
-
-All of this can be easily accomplished without writing any JavaScript at all.
-
-Another benefit of [HTMX](https://htmx.org/) is that it's completely backend-agnostic and does not require any special tools or integrations on the backend. But to make things easier, included is a small package to read and write [HTTP headers](https://htmx.org/docs/#requests) that HTMX uses to communicate additional information and commands.
-
-The `htmx` package contains the headers for the _request_ and _response_. When a `Page` is initialized, `Page.HTMX.Request` will also be initialized and populated with the headers that HTMX provides, if HTMX made the request. This allows you to determine if HTMX is making the given request and what exactly it is doing, which could be useful both in your _route_ as well as your _templates_.
-
-If you need to set any HTMX headers in your `Page` response, this can be done by altering `Page.HTMX.Response`.
-
-#### Layout template override
-
-To faciliate easy partial rendering for HTMX requests, the `Page` will automatically change your _Layout_ template to use `htmx.gohtml`, which currently only renders `{{template "content" .}}`. This allows you to use an HTMX request to only update the content portion of the page, rather than the entire HTML.
-
-This override only happens if the HTMX request being made is **not a boost** request because **boost** requests replace the entire `body` element so there is no need to do a partial render.
-
-#### Conditional processing / rendering
-
-Since HTMX communicates what it is doing with the server, you can use the request headers to conditionally process in your _route_ or render in your _template_, if needed. If your routes aren't doing multiple things, you may not need this, but it's worth knowing how flexible you can be.
-
-A simple example of this:
-
-```go
-if page.HTMX.Request.Target == "search" {
- // You know this request HTMX is fetching content just for the #search element
-}
-```
-
-```go
-{{if eq .page.HTMX.Request.Target "search"}}
- // Render content for the #search element
-{{end}}
-```
-
-### Rendering the page
-
-Once your `Page` is fully built, rendering it via the embedded `Controller` in your _route_ can be done simply by calling `RenderPage()`:
-
-```go
-func (c *Home) Get(ctx echo.Context) error {
- page := controller.NewPage(ctx)
- page.Layout = "main"
- page.Name = "home"
- return c.RenderPage(ctx, page)
-}
-```
-
-## Template renderer
-
-The _template renderer_ is a _Service_ on the `Container` that aims to make template parsing and rendering easy and flexible. It is the mechanism that allows the `Page` to do [automatic template parsing](#automatic-template-parsing). The standard `html/template` is still the engine used behind the scenes. The code can be found in `services/template_renderer.go`.
-
-While there are several methods available, the following is the primary one used:
-
-`ParseAndExecute(cacheGroup, cacheID, baseName string, files []string, directories []string, data interface{})`
-
-Using the example from the [page rendering](#rendering-the-page), this is what the `Controller` will execute:
-
-```go
-buf, err = c.TemplateRenderer.ParseAndExecute(
- "page",
- page.Name,
- page.Layout,
- []string{
- fmt.Sprintf("layouts/%s", page.Layout),
- fmt.Sprintf("pages/%s", page.Name),
- },
- []string{"components"},
- page,
-)
-```
-
-The parameters represent:
- - `cacheGroup`: The _group_ to cache the parsed templates in
- - `cacheID`: The _ID_ of the cache within the _group_
- - `baseName`: The name of the base template, excluding the extension
- - `files`: A list of individual template files to include, excluding the extension and template directory
- - `directories`: A list of directories to include all templates contained
- - `data`: The data object to send to the templates
-
-All templates will be parsed with the [funcap](#funcmap).
-
-### Caching
-
-Parsed templates will be cached within a `sync.Map` so the operation will only happen once per cache _group_ and _ID_. Be careful with your cache _group_ and _ID_ parameters to avoid collisions.
-
-### Hot-reload for development
-
-If the current [environment](#environments) is set to `config.EnvLocal`, which is the default, the cache will be bypassed and templates will be parsed every time they are requested. This allows you to have hot-reloading without having to restart the application so you can see your HTML changes in the browser immediately.
-
-### File configuration
-
-To make things easier and less repetitive, parameters given to the _template renderer_ must not include the `templates` directory or the template file extensions. These are stored as constants within the `config` package. If your project has a need to change either of these, simply adjust the `TemplateDir` and `TemplateExt` constants.
-
-## Funcmap
-
-The `funcmap` package provides a _function map_ (`template.FuncMap`) which will be included for all templates rendered with the [template renderer](#template-renderer). Aside from a few custom functions, [sprig](https://github.com/Masterminds/sprig) is included which provides over 100 commonly used template functions. The full list is available [here](http://masterminds.github.io/sprig/).
-
-To include additional custom functions, add to the slice in `GetFuncMap()` and define the function in the package. It will then become automatically available in all templates.
+There is currently no generic component to easily render a pager, but the homepage does have an example.
## Cache
-As previously mentioned, [Redis](https://redis.io/) was chosen as the cache but it can be easily swapped out for something else. [go-redis](https://github.com/go-redis/redis) is used as the underlying client but the `Container` currently only exposes [gocache](https://github.com/eko/gocache) which was chosen because it makes interfacing with the cache client much easier, and it provides a consistent interface if you were to use a cache backend other than Redis.
+As previously mentioned, the default cache implementation is a simple in-memory store, backed by [otter](https://github.com/maypok86/otter), a lockless cache that uses [S3-FIFO](https://s3fifo.com/) eviction. The `Container` houses a `CacheClient` which is a useful wrapper to interact with the cache (see examples below). Within the `CacheClient` is the underlying store interface `CacheStore`. If you wish to use a different store, such as Redis, and want to keep using the `CacheClient`, simply implement the `CacheStore` interface with a Redis library and adjust the `Container` initialization to use that.
-The built-in usage of the cache is currently only for optional [page caching](#cached-responses) but it can be used for practically anything.
+The built-in usage of the cache is currently only used for a simple example route located at `/cache` where you can set and view the value of a given cache entry.
-## Static files
+Since the current cache is in-memory, there's no need to adjust the `Container` during tests. When this project used Redis, the configuration had a separate database that would be used strictly for tests to avoid writing to your primary database. If you need that functionality, it is easy to add back in.
-Static files are currently configured in the router (`routes/router.go`) to be served from the `static` directory. If you wish to change the directory, alter the constant `config.StaticDir`. The URL prefix for static files is `/files` which is controlled via the `config.StaticPrefix` constant.
+### Set data
-### Cache control headers
+**Set data with just a key:**
-Static files are grouped separately so you can apply middleware only to them. Included is a custom middleware to set cache control headers (`middleware.CacheControl`) which has been added to the static files router group.
-
-The cache max-life is controlled by the configuration at `Config.Cache.Expiration.StaticFile` and defaults to 6 months.
-
-### Cache-buster
-
-While it's ideal to use cache control headers on your static files so browsers cache the files, you need a way to bust the cache in case the files are changed. In order to do this, a function is provided in the [funcmap](#funcmap) to generate a static file URL for a given file that appends a cache-buster query. This query string is randomly generated and persisted until the application restarts.
-
-For example, to render a file located in `static/picture.png`, you would use:
```go
-
+err := c.Cache.
+ Set().
+ Key("my-key").
+ Data(myData).
+ Expiration(time.Hour * 2).
+ Save(ctx)
+```
+
+**Set data within a group:**
+
+```go
+err := c.Cache.
+ Set().
+ Group("my-group").
+ Key("my-key").
+ Expiration(time.Hour * 2).
+ Data(myData).
+ Save(ctx)
+```
+
+**Include cache tags:**
+
+```go
+err := c.Cache.
+ Set().
+ Key("my-key").
+ Tags("tag1", "tag2").
+ Expiration(time.Hour * 2).
+ Data(myData).
+ Save(ctx)
+```
+
+### Get data
+
+```go
+data, err := c.Cache.
+ Get().
+ Group("my-group").
+ Key("my-key").
+ Fetch(ctx)
+```
+
+### Flush data
+
+```go
+err := c.Cache.
+ Flush().
+ Group("my-group").
+ Key("my-key").
+ Execute(ctx)
+```
+
+### Flush tags
+
+This will flush all cache entries that were tagged with the given tags.
+
+```go
+err := c.Cache.
+ Flush().
+ Tags("tag1", "tag2").
+ Execute(ctx)
+```
+
+### Tagging
+
+As shown in the previous examples, cache tags were provided because they can be convenient. However, maintaining them comes at a cost and it may not be a good fit for your application depending on your needs. When including tags, the `CacheClient` must lock in order to keep the tag index in sync. And since the tag index cannot support eviction, since that could result in a flush call not actually flushing the tag's keys, the maps that provide the index do not have a size limit. See the code for more details.
+
+## Tasks
+
+Tasks are queued operations to be executed in the background, either immediately, at a specific time, or after a given amount of time has passed. Some examples of tasks could be long-running operations, bulk processing, cleanup, notifications, etc.
+
+Since we're already using [SQLite](https://sqlite.org/) for our database, it's available to act as a persistent store for queued tasks so that tasks are never lost, can be retried until successful, and their concurrent execution can be managed. [Backlite](https://github.com/mikestefanello/backlite) is the library chosen to interface with [SQLite](https://sqlite.org/) and handle queueing tasks and processing them asynchronously. I wrote that specifically to address the requirements I wanted to satisfy for this project.
+
+To make things easy, the _Backlite_ client is provided as a _Service_ on the `Container` which allows you to register queues and add tasks.
+
+Configuration for the _Backlite_ client is exposed through the app's yaml configuration. The required database schema will be automatically installed when the app starts.
+
+### Queues
+
+A full example of a queue implementation can be found in `pkg/tasks` with an interactive form to create a task and add to the queue at `/task` (see `pkg/handlers/task.go`). Also refer to the [Backlite](https://github.com/mikestefanello/backlite) documentation for reference and examples.
+
+See `pkg/tasks/register.go` for a simple way to register all of your queues and to easily pass the `Container` to them so the queue processor callbacks have access to all of your app's dependencies.
+
+### Dispatcher
+
+The _task dispatcher_ is what manages the worker pool used for executing tasks in the background. It monitors incoming and scheduled tasks and handles sending them to the pool for execution by the queue's processor callback. This must be started in order for this to happen. In `cmd/web/main.go`, the _task dispatcher_ is automatically started when the app starts via:
+
+```go
+c.Tasks.Start(ctx)
+```
+
+The app [configuration](#configuration) contains values to configure the client and dispatcher including how many goroutines to use, when to release stuck tasks back into the queue, and how often to cleanup retained tasks in the database.
+
+When the app is shutdown, the dispatcher is given 10 seconds to wait for any in-progress tasks to finish execution. This can be changed in `cmd/web/main.go`.
+
+### Monitoring tasks and queues
+
+The [admin panel](#admin-panel) contains the UI provided by [Backlite](https://github.com/mikestefanello/backlite) in order to fully monitor all tasks and queues from within your browser.
+
+## Cron
+
+By default, no cron solution is provided because it's very easy to add yourself if you need this. You can either use a [ticker](https://pkg.go.dev/time#Ticker) or a [library](https://github.com/robfig/cron).
+
+## Files
+
+Providing a generic approach to files that works for all use-cases is nearly impossible, so the defaults provided here is more of an illustration of what features are available. It's likely you will want to use a CDN and/or cloud storage for all or some of your file management.
+
+### Uploads
+
+To handle file management functionality such as file uploads, an abstracted file system interface is provided as a _Service_ on the `Container` powered by [afero](https://github.com/spf13/afero). This allows you to easily change the file system backend (ie, local, GCS, SFTP, in-memory) without having to change any of the application code other than the initialization on the `Container`. By default, the local OS is used with a directory specified in the application configuration (which defaults to `uploads`). When running tests, an in-memory file system backend is automatically used.
+
+A simple file upload form example is provided at `/files` which also dynamically lists all files previously uploaded. No database entities or entries are created or provided for files and uploaded files are not available to be served. You will have to implement whatever functionality your application needs.
+
+### Static files
+
+Static files, which are intended to be CSS, JS, UI images, etc, are currently configured in the router (`pkg/handler/router.go`) to be served from the `public/static` directory and are available via the URL prefix `/static`. By default, `make css` will write your css file here.
+
+#### Cache-buster
+
+While it's ideal to use cache control headers on your static files so browsers cache the files, you need a way to bust the cache in case the files are changed. To do this, a function, `StaticFile()`, is provided in the `ui` package to generate a static file URL for a given file that appends a cache-buster query. This query string is generated using the timestamp of when the app started and persists until the application restarts.
+
+For example, to render a file located in `public/static/picture.png`, you would use:
+```go
+return Img(Src(ui.StaticFile("picture.png")))
```
Which would result in:
```html
-
+
```
-Where `9fhe73kaf3` is the randomly-generated cache-buster.
+Where `1741053493` is the cache-buster.
+
+### Public files
+
+An additional option for public files that do not fit in to the previously mentioned categories is provided as an example. Defined in the router, files located in `public/files` are available via the URL prefix `/files`. `ui.PublicFile()` can be used to generate URLs for files within this directory.
+
+### Cache control headers
+
+Static and public files are grouped separately so you can apply middleware only to them. Included is a custom middleware to set cache control headers (`middleware.CacheControl`) which has been added to the static files router group.
+
+The cache max-life is controlled by the configuration at `Config.Cache.Expiration.PublicFile` and defaults to 6 months.
## 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.
+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 that 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.
+The structure in the client (`MailClient`) makes composing emails very easy, and you have the option to construct the body using either a simple string or with a renderable _gomponent_, as explained in the [user interface](#user-interface), in order to produce HTML emails. A simple example is provided in `pkg/ui/emails`.
+
+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. **You must** finish the implementation of `MailClient.send`.
+
+The _from_ address will default to the configuration value at `Config.Mail.FromAddress`. This can be overridden per-email by calling `From()` on the email and passing in the desired address.
+
+See below for examples on how to use the client to compose emails.
+
+**Sending with a string body**:
+
+```go
+err = c.Mail.
+ Compose().
+ To("hello@example.com").
+ Subject("Welcome!").
+ Body("Thank you for registering.").
+ Send(ctx)
+```
+
+**Sending an HTML body using a gomponent**:
+
+```go
+err = c.Mail.
+ Compose().
+ To("hello@example.com").
+ Subject("Confirm your email address").
+ Component(emails.ConfirmEmailAddress(ctx, username, token)).
+ Send(ctx)
+```
+
+This will use the HTML provided when rendering the _gomponent_ as the email body.
## HTTPS
@@ -871,52 +1161,71 @@ To use _Let's Encrypt_ follow [this guide](https://echo.labstack.com/cookbook/au
## 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:
+By default, the [Echo logger](https://echo.labstack.com/guide/customization/#logging) is not used for the following reasons:
+1) It does not support structured logging, which makes it difficult to deal with variables, especially if you intend to store a logger in context with pre-populated attributes.
+2) The upcoming v5 release of Echo will not contain a logger.
+3) It should be easy to use whatever logger you prefer (even if that is Echo's logger).
+
+The provided implementation uses the relatively new [log/slog](https://go.dev/blog/slog) library which was added to Go in version 1.21 but swapping that out for whichever you prefer is very easy.
+
+### Context
+
+The simple `pkg/log` package provides the ability to set and get a logger from the Echo context. This is especially useful when you use the provided logger middleware (see below). If you intend to use a different logger, modify these methods to receive and return the logger of your choice.
+
+### Usage
+
+Adding a logger to the context:
```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)
- }
+logger := slog.New(logHandler).With("id", requestId)
+log.Set(ctx, logger)
+```
+
+Access and use the logger:
+```go
+func (h *handler) Page(ctx echo.Context) error {
+ log.Ctx(ctx).Info("send a message to the log",
+ "value_one", valueOne,
+ "value_two", valueTwo,
+ )
}
```
-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).
+### Log level
-### Request ID
+When the _Container_ configuration is initialized (`initConfig()`), the `slog` default log level is set based on the environment. `INFO` is used for production and `DEBUG` for everything else.
-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.
+### Middleware
-## Roadmap
+The `SetLogger()` middleware has been added to the router which sets an initialized logger on the request context. It's recommended that this remains after Echo's `RequestID()` middleware because it will add the request ID to the logger which means that all logs produced for that given request will contain the same ID, so they can be linked together. If you want to include more attributes on all request logs, set those fields here.
-Future work includes but is not limited to:
+The `LogRequest()` middleware is a replacement for Echo's `Logger()` middleware which produces a log of every request made, but uses our logger rather than Echo's.
-- Email verification
-- Flexible pager templates
-- Expanded HTMX examples
-- Admin section
+```
+2024/06/15 09:07:11 INFO GET /contact request_id=gNblvugTKcyLnBYPMPTwMPEqDOioVLKp ip=::1 host=localhost:8000 referer="" status=200 bytes_in=0 bytes_out=5925 latency=107.527803ms
+```
## Credits
-Thank you to all of the following amazing projects for making this possible.
+Thank you to all the following amazing projects for making this possible.
-- [go](https://go.dev/)
+- [afero](https://github.com/spf13/afero)
+- [air](https://github.com/air-verse/air)
+- [alpinejs](https://github.com/alpinejs/alpine)
+- [backlite](https://github.com/mikestefanello/backlite)
+- [daisyui](https://github.com/saadeghi/daisyui)
- [echo](https://github.com/labstack/echo)
- [ent](https://github.com/ent/ent)
-- [sprig](https://github.com/Masterminds/sprig)
+- [go](https://go.dev/)
+- [go-sqlite3](https://github.com/mattn/go-sqlite3)
+- [gomponents](https://github.com/maragudk/gomponents)
- [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
+- [jwt](https://github.com/golang-jwt/jwt)
+- [otter](https://github.com/maypok86/otter)
+- [sessions](https://github.com/gorilla/sessions)
+- [sqlite](https://sqlite.org/)
+- [tailwindcss](https://github.com/tailwindlabs/tailwindcss)
+- [testify](https://github.com/stretchr/testify)
+- [validator](https://github.com/go-playground/validator)
+- [viper](https://github.com/spf13/viper)
\ No newline at end of file
diff --git a/cmd/admin/main.go b/cmd/admin/main.go
new file mode 100644
index 0000000..449e4a8
--- /dev/null
+++ b/cmd/admin/main.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "github.com/mikestefanello/pagoda/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..a04c134
--- /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/mikestefanello/pagoda/pkg/handlers"
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/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
deleted file mode 100644
index ad78bd2..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-version: "3"
-
-services:
- cache:
- image: "redis:alpine"
- ports:
- - "6379:6379"
- db:
- image: postgres:alpine
- ports:
- - "5432:5432"
- environment:
- - POSTGRES_USER=admin
- - POSTGRES_PASSWORD=admin
- - POSTGRES_DB=app
\ No newline at end of file
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..bb74b15
--- /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/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/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..126d531 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/mikestefanello/pagoda/ent/migrate"
+ "entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/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..ff6dafb 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/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/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..fdf2308
--- /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/mikestefanello/pagoda/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..d43e013 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/mikestefanello/pagoda/ent"
// required by schema hooks.
- _ "goweb/ent/runtime"
+ _ "github.com/mikestefanello/pagoda/ent/runtime"
"entgo.io/ent/dialect/sql/schema"
+ "github.com/mikestefanello/pagoda/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..cea63fc 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/mikestefanello/pagoda/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..37d071d 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/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/predicate"
+ "github.com/mikestefanello/pagoda/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..fec22ac 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/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/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=")
- builder.WriteString(", created_at=")
- builder.WriteString(pt.CreatedAt.Format(time.ANSIC))
+ builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
+ builder.WriteString("token=")
+ builder.WriteString(", ")
+ builder.WriteString("user_id=")
+ builder.WriteString(fmt.Sprintf("%v", _m.UserID))
+ builder.WriteString(", ")
+ builder.WriteString("created_at=")
+ builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
builder.WriteByte(')')
return builder.String()
}
// PasswordTokens is a parsable slice of PasswordToken.
type PasswordTokens []*PasswordToken
-
-func (pt PasswordTokens) config(cfg config) {
- for _i := range pt {
- pt[_i].config = cfg
- }
-}
diff --git a/ent/passwordtoken/passwordtoken.go b/ent/passwordtoken/passwordtoken.go
index 56bff22..ea109f7 100644
--- a/ent/passwordtoken/passwordtoken.go
+++ b/ent/passwordtoken/passwordtoken.go
@@ -1,9 +1,13 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package passwordtoken
import (
"time"
+
+ "entgo.io/ent"
+ "entgo.io/ent/dialect/sql"
+ "entgo.io/ent/dialect/sql/sqlgraph"
)
const (
@@ -11,8 +15,10 @@ const (
Label = "password_token"
// FieldID holds the string denoting the id field in the database.
FieldID = "id"
- // FieldHash holds the string denoting the hash field in the database.
- FieldHash = "hash"
+ // FieldToken holds the string denoting the token field in the database.
+ FieldToken = "token"
+ // FieldUserID holds the string denoting the user_id field in the database.
+ FieldUserID = "user_id"
// FieldCreatedAt holds the string denoting the created_at field in the database.
FieldCreatedAt = "created_at"
// EdgeUser holds the string denoting the user edge name in mutations.
@@ -25,22 +31,17 @@ const (
// It exists in this package in order to avoid circular dependency with the "user" package.
UserInverseTable = "users"
// UserColumn is the table column denoting the user relation/edge.
- UserColumn = "password_token_user"
+ UserColumn = "user_id"
)
// Columns holds all SQL columns for passwordtoken fields.
var Columns = []string{
FieldID,
- FieldHash,
+ FieldToken,
+ FieldUserID,
FieldCreatedAt,
}
-// ForeignKeys holds the SQL foreign-keys that are owned by the "password_tokens"
-// table and are not defined as standalone fields in the schema.
-var ForeignKeys = []string{
- "password_token_user",
-}
-
// ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool {
for i := range Columns {
@@ -48,17 +49,55 @@ func ValidColumn(column string) bool {
return true
}
}
- for i := range ForeignKeys {
- if column == ForeignKeys[i] {
- return true
- }
- }
return false
}
+// Note that the variables below are initialized by the runtime
+// package on the initialization of the application. Therefore,
+// it should be imported in the main as follows:
+//
+// import _ "github.com/mikestefanello/pagoda/ent/runtime"
var (
- // HashValidator is a validator for the "hash" field. It is called by the builders before save.
- HashValidator func(string) error
+ Hooks [1]ent.Hook
+ // TokenValidator is a validator for the "token" field. It is called by the builders before save.
+ TokenValidator func(string) error
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
DefaultCreatedAt func() time.Time
)
+
+// OrderOption defines the ordering options for the PasswordToken queries.
+type OrderOption func(*sql.Selector)
+
+// ByID orders the results by the id field.
+func ByID(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldID, opts...).ToFunc()
+}
+
+// ByToken orders the results by the token field.
+func ByToken(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldToken, opts...).ToFunc()
+}
+
+// ByUserID orders the results by the user_id field.
+func ByUserID(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldUserID, opts...).ToFunc()
+}
+
+// ByCreatedAt orders the results by the created_at field.
+func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
+}
+
+// ByUserField orders the results by user field.
+func ByUserField(field string, opts ...sql.OrderTermOption) OrderOption {
+ return func(s *sql.Selector) {
+ sqlgraph.OrderByNeighborTerms(s, newUserStep(), sql.OrderByField(field, opts...))
+ }
+}
+func newUserStep() *sqlgraph.Step {
+ return sqlgraph.NewStep(
+ sqlgraph.From(Table, FieldID),
+ sqlgraph.To(UserInverseTable, FieldID),
+ sqlgraph.Edge(sqlgraph.M2O, false, UserTable, UserColumn),
+ )
+}
diff --git a/ent/passwordtoken/where.go b/ent/passwordtoken/where.go
index cec00b7..d563d01 100644
--- a/ent/passwordtoken/where.go
+++ b/ent/passwordtoken/where.go
@@ -1,297 +1,198 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package passwordtoken
import (
- "goweb/ent/predicate"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
+ "github.com/mikestefanello/pagoda/ent/predicate"
)
// ID filters vertices based on their ID field.
func ID(id int) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldID), id))
- })
+ return predicate.PasswordToken(sql.FieldEQ(FieldID, id))
}
// IDEQ applies the EQ predicate on the ID field.
func IDEQ(id int) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldID), id))
- })
+ return predicate.PasswordToken(sql.FieldEQ(FieldID, id))
}
// IDNEQ applies the NEQ predicate on the ID field.
func IDNEQ(id int) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.NEQ(s.C(FieldID), id))
- })
+ return predicate.PasswordToken(sql.FieldNEQ(FieldID, id))
}
// IDIn applies the In predicate on the ID field.
func IDIn(ids ...int) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(ids) == 0 {
- s.Where(sql.False())
- return
- }
- v := make([]interface{}, len(ids))
- for i := range v {
- v[i] = ids[i]
- }
- s.Where(sql.In(s.C(FieldID), v...))
- })
+ return predicate.PasswordToken(sql.FieldIn(FieldID, ids...))
}
// IDNotIn applies the NotIn predicate on the ID field.
func IDNotIn(ids ...int) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(ids) == 0 {
- s.Where(sql.False())
- return
- }
- v := make([]interface{}, len(ids))
- for i := range v {
- v[i] = ids[i]
- }
- s.Where(sql.NotIn(s.C(FieldID), v...))
- })
+ return predicate.PasswordToken(sql.FieldNotIn(FieldID, ids...))
}
// IDGT applies the GT predicate on the ID field.
func IDGT(id int) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.GT(s.C(FieldID), id))
- })
+ return predicate.PasswordToken(sql.FieldGT(FieldID, id))
}
// IDGTE applies the GTE predicate on the ID field.
func IDGTE(id int) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.GTE(s.C(FieldID), id))
- })
+ return predicate.PasswordToken(sql.FieldGTE(FieldID, id))
}
// IDLT applies the LT predicate on the ID field.
func IDLT(id int) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.LT(s.C(FieldID), id))
- })
+ return predicate.PasswordToken(sql.FieldLT(FieldID, id))
}
// IDLTE applies the LTE predicate on the ID field.
func IDLTE(id int) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.LTE(s.C(FieldID), id))
- })
+ return predicate.PasswordToken(sql.FieldLTE(FieldID, id))
}
-// Hash applies equality check predicate on the "hash" field. It's identical to HashEQ.
-func Hash(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldHash), v))
- })
+// Token applies equality check predicate on the "token" field. It's identical to TokenEQ.
+func Token(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldEQ(FieldToken, v))
+}
+
+// UserID applies equality check predicate on the "user_id" field. It's identical to UserIDEQ.
+func UserID(v int) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldEQ(FieldUserID, v))
}
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
func CreatedAt(v time.Time) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldCreatedAt), v))
- })
+ return predicate.PasswordToken(sql.FieldEQ(FieldCreatedAt, v))
}
-// HashEQ applies the EQ predicate on the "hash" field.
-func HashEQ(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldHash), v))
- })
+// TokenEQ applies the EQ predicate on the "token" field.
+func TokenEQ(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldEQ(FieldToken, v))
}
-// HashNEQ applies the NEQ predicate on the "hash" field.
-func HashNEQ(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.NEQ(s.C(FieldHash), v))
- })
+// TokenNEQ applies the NEQ predicate on the "token" field.
+func TokenNEQ(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldNEQ(FieldToken, v))
}
-// HashIn applies the In predicate on the "hash" field.
-func HashIn(vs ...string) predicate.PasswordToken {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.PasswordToken(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.In(s.C(FieldHash), v...))
- })
+// TokenIn applies the In predicate on the "token" field.
+func TokenIn(vs ...string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldIn(FieldToken, vs...))
}
-// HashNotIn applies the NotIn predicate on the "hash" field.
-func HashNotIn(vs ...string) predicate.PasswordToken {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.PasswordToken(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.NotIn(s.C(FieldHash), v...))
- })
+// TokenNotIn applies the NotIn predicate on the "token" field.
+func TokenNotIn(vs ...string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldNotIn(FieldToken, vs...))
}
-// HashGT applies the GT predicate on the "hash" field.
-func HashGT(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.GT(s.C(FieldHash), v))
- })
+// TokenGT applies the GT predicate on the "token" field.
+func TokenGT(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldGT(FieldToken, v))
}
-// HashGTE applies the GTE predicate on the "hash" field.
-func HashGTE(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.GTE(s.C(FieldHash), v))
- })
+// TokenGTE applies the GTE predicate on the "token" field.
+func TokenGTE(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldGTE(FieldToken, v))
}
-// HashLT applies the LT predicate on the "hash" field.
-func HashLT(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.LT(s.C(FieldHash), v))
- })
+// TokenLT applies the LT predicate on the "token" field.
+func TokenLT(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldLT(FieldToken, v))
}
-// HashLTE applies the LTE predicate on the "hash" field.
-func HashLTE(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.LTE(s.C(FieldHash), v))
- })
+// TokenLTE applies the LTE predicate on the "token" field.
+func TokenLTE(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldLTE(FieldToken, v))
}
-// HashContains applies the Contains predicate on the "hash" field.
-func HashContains(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.Contains(s.C(FieldHash), v))
- })
+// TokenContains applies the Contains predicate on the "token" field.
+func TokenContains(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldContains(FieldToken, v))
}
-// HashHasPrefix applies the HasPrefix predicate on the "hash" field.
-func HashHasPrefix(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.HasPrefix(s.C(FieldHash), v))
- })
+// TokenHasPrefix applies the HasPrefix predicate on the "token" field.
+func TokenHasPrefix(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldHasPrefix(FieldToken, v))
}
-// HashHasSuffix applies the HasSuffix predicate on the "hash" field.
-func HashHasSuffix(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.HasSuffix(s.C(FieldHash), v))
- })
+// TokenHasSuffix applies the HasSuffix predicate on the "token" field.
+func TokenHasSuffix(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldHasSuffix(FieldToken, v))
}
-// HashEqualFold applies the EqualFold predicate on the "hash" field.
-func HashEqualFold(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.EqualFold(s.C(FieldHash), v))
- })
+// TokenEqualFold applies the EqualFold predicate on the "token" field.
+func TokenEqualFold(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldEqualFold(FieldToken, v))
}
-// HashContainsFold applies the ContainsFold predicate on the "hash" field.
-func HashContainsFold(v string) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.ContainsFold(s.C(FieldHash), v))
- })
+// TokenContainsFold applies the ContainsFold predicate on the "token" field.
+func TokenContainsFold(v string) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldContainsFold(FieldToken, v))
+}
+
+// UserIDEQ applies the EQ predicate on the "user_id" field.
+func UserIDEQ(v int) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldEQ(FieldUserID, v))
+}
+
+// UserIDNEQ applies the NEQ predicate on the "user_id" field.
+func UserIDNEQ(v int) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldNEQ(FieldUserID, v))
+}
+
+// UserIDIn applies the In predicate on the "user_id" field.
+func UserIDIn(vs ...int) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldIn(FieldUserID, vs...))
+}
+
+// UserIDNotIn applies the NotIn predicate on the "user_id" field.
+func UserIDNotIn(vs ...int) predicate.PasswordToken {
+ return predicate.PasswordToken(sql.FieldNotIn(FieldUserID, vs...))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldCreatedAt), v))
- })
+ return predicate.PasswordToken(sql.FieldEQ(FieldCreatedAt, v))
}
// CreatedAtNEQ applies the NEQ predicate on the "created_at" field.
func CreatedAtNEQ(v time.Time) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.NEQ(s.C(FieldCreatedAt), v))
- })
+ return predicate.PasswordToken(sql.FieldNEQ(FieldCreatedAt, v))
}
// CreatedAtIn applies the In predicate on the "created_at" field.
func CreatedAtIn(vs ...time.Time) predicate.PasswordToken {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.PasswordToken(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.In(s.C(FieldCreatedAt), v...))
- })
+ return predicate.PasswordToken(sql.FieldIn(FieldCreatedAt, vs...))
}
// CreatedAtNotIn applies the NotIn predicate on the "created_at" field.
func CreatedAtNotIn(vs ...time.Time) predicate.PasswordToken {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.PasswordToken(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.NotIn(s.C(FieldCreatedAt), v...))
- })
+ return predicate.PasswordToken(sql.FieldNotIn(FieldCreatedAt, vs...))
}
// CreatedAtGT applies the GT predicate on the "created_at" field.
func CreatedAtGT(v time.Time) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.GT(s.C(FieldCreatedAt), v))
- })
+ return predicate.PasswordToken(sql.FieldGT(FieldCreatedAt, v))
}
// CreatedAtGTE applies the GTE predicate on the "created_at" field.
func CreatedAtGTE(v time.Time) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.GTE(s.C(FieldCreatedAt), v))
- })
+ return predicate.PasswordToken(sql.FieldGTE(FieldCreatedAt, v))
}
// CreatedAtLT applies the LT predicate on the "created_at" field.
func CreatedAtLT(v time.Time) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.LT(s.C(FieldCreatedAt), v))
- })
+ return predicate.PasswordToken(sql.FieldLT(FieldCreatedAt, v))
}
// CreatedAtLTE applies the LTE predicate on the "created_at" field.
func CreatedAtLTE(v time.Time) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.LTE(s.C(FieldCreatedAt), v))
- })
+ return predicate.PasswordToken(sql.FieldLTE(FieldCreatedAt, v))
}
// HasUser applies the HasEdge predicate on the "user" edge.
@@ -299,7 +200,6 @@ func HasUser() predicate.PasswordToken {
return predicate.PasswordToken(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
- sqlgraph.To(UserTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, false, UserTable, UserColumn),
)
sqlgraph.HasNeighbors(s, step)
@@ -309,11 +209,7 @@ func HasUser() predicate.PasswordToken {
// HasUserWith applies the HasEdge predicate on the "user" edge with a given conditions (other predicates).
func HasUserWith(preds ...predicate.User) predicate.PasswordToken {
return predicate.PasswordToken(func(s *sql.Selector) {
- step := sqlgraph.NewStep(
- sqlgraph.From(Table, FieldID),
- sqlgraph.To(UserInverseTable, FieldID),
- sqlgraph.Edge(sqlgraph.M2O, false, UserTable, UserColumn),
- )
+ step := newUserStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
@@ -324,32 +220,15 @@ func HasUserWith(preds ...predicate.User) predicate.PasswordToken {
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.PasswordToken) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s1 := s.Clone().SetP(nil)
- for _, p := range predicates {
- p(s1)
- }
- s.Where(s1.P())
- })
+ return predicate.PasswordToken(sql.AndPredicates(predicates...))
}
// Or groups predicates with the OR operator between them.
func Or(predicates ...predicate.PasswordToken) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- s1 := s.Clone().SetP(nil)
- for i, p := range predicates {
- if i > 0 {
- s1.Or()
- }
- p(s1)
- }
- s.Where(s1.P())
- })
+ return predicate.PasswordToken(sql.OrPredicates(predicates...))
}
// Not applies the not operator on the given predicate.
func Not(p predicate.PasswordToken) predicate.PasswordToken {
- return predicate.PasswordToken(func(s *sql.Selector) {
- p(s.Not())
- })
+ return predicate.PasswordToken(sql.NotPredicates(p))
}
diff --git a/ent/passwordtoken_create.go b/ent/passwordtoken_create.go
index 3a7614f..e5bb2dd 100644
--- a/ent/passwordtoken_create.go
+++ b/ent/passwordtoken_create.go
@@ -1,4 +1,4 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
@@ -6,12 +6,12 @@ import (
"context"
"errors"
"fmt"
- "goweb/ent/passwordtoken"
- "goweb/ent/user"
"time"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/user"
)
// PasswordTokenCreate is the builder for creating a PasswordToken entity.
@@ -21,87 +21,53 @@ type PasswordTokenCreate struct {
hooks []Hook
}
-// SetHash sets the "hash" field.
-func (ptc *PasswordTokenCreate) SetHash(s string) *PasswordTokenCreate {
- ptc.mutation.SetHash(s)
- return ptc
+// SetToken sets the "token" field.
+func (_c *PasswordTokenCreate) SetToken(v string) *PasswordTokenCreate {
+ _c.mutation.SetToken(v)
+ return _c
+}
+
+// SetUserID sets the "user_id" field.
+func (_c *PasswordTokenCreate) SetUserID(v int) *PasswordTokenCreate {
+ _c.mutation.SetUserID(v)
+ return _c
}
// SetCreatedAt sets the "created_at" field.
-func (ptc *PasswordTokenCreate) SetCreatedAt(t time.Time) *PasswordTokenCreate {
- ptc.mutation.SetCreatedAt(t)
- return ptc
+func (_c *PasswordTokenCreate) SetCreatedAt(v time.Time) *PasswordTokenCreate {
+ _c.mutation.SetCreatedAt(v)
+ return _c
}
// SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
-func (ptc *PasswordTokenCreate) SetNillableCreatedAt(t *time.Time) *PasswordTokenCreate {
- if t != nil {
- ptc.SetCreatedAt(*t)
+func (_c *PasswordTokenCreate) SetNillableCreatedAt(v *time.Time) *PasswordTokenCreate {
+ if v != nil {
+ _c.SetCreatedAt(*v)
}
- return ptc
-}
-
-// SetUserID sets the "user" edge to the User entity by ID.
-func (ptc *PasswordTokenCreate) SetUserID(id int) *PasswordTokenCreate {
- ptc.mutation.SetUserID(id)
- return ptc
+ return _c
}
// SetUser sets the "user" edge to the User entity.
-func (ptc *PasswordTokenCreate) SetUser(u *User) *PasswordTokenCreate {
- return ptc.SetUserID(u.ID)
+func (_c *PasswordTokenCreate) SetUser(v *User) *PasswordTokenCreate {
+ return _c.SetUserID(v.ID)
}
// Mutation returns the PasswordTokenMutation object of the builder.
-func (ptc *PasswordTokenCreate) Mutation() *PasswordTokenMutation {
- return ptc.mutation
+func (_c *PasswordTokenCreate) Mutation() *PasswordTokenMutation {
+ return _c.mutation
}
// Save creates the PasswordToken in the database.
-func (ptc *PasswordTokenCreate) Save(ctx context.Context) (*PasswordToken, error) {
- var (
- err error
- node *PasswordToken
- )
- ptc.defaults()
- if len(ptc.hooks) == 0 {
- if err = ptc.check(); err != nil {
- return nil, err
- }
- node, err = ptc.sqlSave(ctx)
- } else {
- var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
- mutation, ok := m.(*PasswordTokenMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T", m)
- }
- if err = ptc.check(); err != nil {
- return nil, err
- }
- ptc.mutation = mutation
- if node, err = ptc.sqlSave(ctx); err != nil {
- return nil, err
- }
- mutation.id = &node.ID
- mutation.done = true
- return node, err
- })
- for i := len(ptc.hooks) - 1; i >= 0; i-- {
- if ptc.hooks[i] == nil {
- return nil, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)")
- }
- mut = ptc.hooks[i](mut)
- }
- if _, err := mut.Mutate(ctx, ptc.mutation); err != nil {
- return nil, err
- }
+func (_c *PasswordTokenCreate) Save(ctx context.Context) (*PasswordToken, error) {
+ if err := _c.defaults(); err != nil {
+ return nil, err
}
- return node, err
+ return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks)
}
// SaveX calls Save and panics if Save returns an error.
-func (ptc *PasswordTokenCreate) SaveX(ctx context.Context) *PasswordToken {
- v, err := ptc.Save(ctx)
+func (_c *PasswordTokenCreate) SaveX(ctx context.Context) *PasswordToken {
+ v, err := _c.Save(ctx)
if err != nil {
panic(err)
}
@@ -109,86 +75,84 @@ func (ptc *PasswordTokenCreate) SaveX(ctx context.Context) *PasswordToken {
}
// Exec executes the query.
-func (ptc *PasswordTokenCreate) Exec(ctx context.Context) error {
- _, err := ptc.Save(ctx)
+func (_c *PasswordTokenCreate) Exec(ctx context.Context) error {
+ _, err := _c.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
-func (ptc *PasswordTokenCreate) ExecX(ctx context.Context) {
- if err := ptc.Exec(ctx); err != nil {
+func (_c *PasswordTokenCreate) ExecX(ctx context.Context) {
+ if err := _c.Exec(ctx); err != nil {
panic(err)
}
}
// defaults sets the default values of the builder before save.
-func (ptc *PasswordTokenCreate) defaults() {
- if _, ok := ptc.mutation.CreatedAt(); !ok {
- v := passwordtoken.DefaultCreatedAt()
- ptc.mutation.SetCreatedAt(v)
- }
-}
-
-// check runs all checks and user-defined validators on the builder.
-func (ptc *PasswordTokenCreate) check() error {
- if _, ok := ptc.mutation.Hash(); !ok {
- return &ValidationError{Name: "hash", err: errors.New(`ent: missing required field "hash"`)}
- }
- if v, ok := ptc.mutation.Hash(); ok {
- if err := passwordtoken.HashValidator(v); err != nil {
- return &ValidationError{Name: "hash", err: fmt.Errorf(`ent: validator failed for field "hash": %w`, err)}
+func (_c *PasswordTokenCreate) defaults() error {
+ if _, ok := _c.mutation.CreatedAt(); !ok {
+ if passwordtoken.DefaultCreatedAt == nil {
+ return fmt.Errorf("ent: uninitialized passwordtoken.DefaultCreatedAt (forgotten import ent/runtime?)")
}
- }
- if _, ok := ptc.mutation.CreatedAt(); !ok {
- return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "created_at"`)}
- }
- if _, ok := ptc.mutation.UserID(); !ok {
- return &ValidationError{Name: "user", err: errors.New("ent: missing required edge \"user\"")}
+ v := passwordtoken.DefaultCreatedAt()
+ _c.mutation.SetCreatedAt(v)
}
return nil
}
-func (ptc *PasswordTokenCreate) sqlSave(ctx context.Context) (*PasswordToken, error) {
- _node, _spec := ptc.createSpec()
- if err := sqlgraph.CreateNode(ctx, ptc.driver, _spec); err != nil {
+// check runs all checks and user-defined validators on the builder.
+func (_c *PasswordTokenCreate) check() error {
+ if _, ok := _c.mutation.Token(); !ok {
+ return &ValidationError{Name: "token", err: errors.New(`ent: missing required field "PasswordToken.token"`)}
+ }
+ if v, ok := _c.mutation.Token(); ok {
+ if err := passwordtoken.TokenValidator(v); err != nil {
+ return &ValidationError{Name: "token", err: fmt.Errorf(`ent: validator failed for field "PasswordToken.token": %w`, err)}
+ }
+ }
+ if _, ok := _c.mutation.UserID(); !ok {
+ return &ValidationError{Name: "user_id", err: errors.New(`ent: missing required field "PasswordToken.user_id"`)}
+ }
+ if _, ok := _c.mutation.CreatedAt(); !ok {
+ return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "PasswordToken.created_at"`)}
+ }
+ if len(_c.mutation.UserIDs()) == 0 {
+ return &ValidationError{Name: "user", err: errors.New(`ent: missing required edge "PasswordToken.user"`)}
+ }
+ return nil
+}
+
+func (_c *PasswordTokenCreate) sqlSave(ctx context.Context) (*PasswordToken, error) {
+ if err := _c.check(); err != nil {
+ return nil, err
+ }
+ _node, _spec := _c.createSpec()
+ if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil {
if sqlgraph.IsConstraintError(err) {
- err = &ConstraintError{err.Error(), err}
+ err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
id := _spec.ID.Value.(int64)
_node.ID = int(id)
+ _c.mutation.id = &_node.ID
+ _c.mutation.done = true
return _node, nil
}
-func (ptc *PasswordTokenCreate) createSpec() (*PasswordToken, *sqlgraph.CreateSpec) {
+func (_c *PasswordTokenCreate) createSpec() (*PasswordToken, *sqlgraph.CreateSpec) {
var (
- _node = &PasswordToken{config: ptc.config}
- _spec = &sqlgraph.CreateSpec{
- Table: passwordtoken.Table,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
- }
+ _node = &PasswordToken{config: _c.config}
+ _spec = sqlgraph.NewCreateSpec(passwordtoken.Table, sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt))
)
- if value, ok := ptc.mutation.Hash(); ok {
- _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: passwordtoken.FieldHash,
- })
- _node.Hash = value
+ if value, ok := _c.mutation.Token(); ok {
+ _spec.SetField(passwordtoken.FieldToken, field.TypeString, value)
+ _node.Token = value
}
- if value, ok := ptc.mutation.CreatedAt(); ok {
- _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
- Type: field.TypeTime,
- Value: value,
- Column: passwordtoken.FieldCreatedAt,
- })
+ if value, ok := _c.mutation.CreatedAt(); ok {
+ _spec.SetField(passwordtoken.FieldCreatedAt, field.TypeTime, value)
_node.CreatedAt = value
}
- if nodes := ptc.mutation.UserIDs(); len(nodes) > 0 {
+ if nodes := _c.mutation.UserIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
@@ -196,16 +160,13 @@ func (ptc *PasswordTokenCreate) createSpec() (*PasswordToken, *sqlgraph.CreateSp
Columns: []string{passwordtoken.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
- _node.password_token_user = &nodes[0]
+ _node.UserID = nodes[0]
_spec.Edges = append(_spec.Edges, edge)
}
return _node, _spec
@@ -214,17 +175,21 @@ func (ptc *PasswordTokenCreate) createSpec() (*PasswordToken, *sqlgraph.CreateSp
// PasswordTokenCreateBulk is the builder for creating many PasswordToken entities in bulk.
type PasswordTokenCreateBulk struct {
config
+ err error
builders []*PasswordTokenCreate
}
// Save creates the PasswordToken entities in the database.
-func (ptcb *PasswordTokenCreateBulk) Save(ctx context.Context) ([]*PasswordToken, error) {
- specs := make([]*sqlgraph.CreateSpec, len(ptcb.builders))
- nodes := make([]*PasswordToken, len(ptcb.builders))
- mutators := make([]Mutator, len(ptcb.builders))
- for i := range ptcb.builders {
+func (_c *PasswordTokenCreateBulk) Save(ctx context.Context) ([]*PasswordToken, error) {
+ if _c.err != nil {
+ return nil, _c.err
+ }
+ specs := make([]*sqlgraph.CreateSpec, len(_c.builders))
+ nodes := make([]*PasswordToken, len(_c.builders))
+ mutators := make([]Mutator, len(_c.builders))
+ for i := range _c.builders {
func(i int, root context.Context) {
- builder := ptcb.builders[i]
+ builder := _c.builders[i]
builder.defaults()
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
mutation, ok := m.(*PasswordTokenMutation)
@@ -235,16 +200,16 @@ func (ptcb *PasswordTokenCreateBulk) Save(ctx context.Context) ([]*PasswordToken
return nil, err
}
builder.mutation = mutation
- nodes[i], specs[i] = builder.createSpec()
var err error
+ nodes[i], specs[i] = builder.createSpec()
if i < len(mutators)-1 {
- _, err = mutators[i+1].Mutate(root, ptcb.builders[i+1].mutation)
+ _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation)
} else {
spec := &sqlgraph.BatchCreateSpec{Nodes: specs}
// Invoke the actual operation on the latest mutation in the chain.
- if err = sqlgraph.BatchCreate(ctx, ptcb.driver, spec); err != nil {
+ if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil {
if sqlgraph.IsConstraintError(err) {
- err = &ConstraintError{err.Error(), err}
+ err = &ConstraintError{msg: err.Error(), wrap: err}
}
}
}
@@ -252,11 +217,11 @@ func (ptcb *PasswordTokenCreateBulk) Save(ctx context.Context) ([]*PasswordToken
return nil, err
}
mutation.id = &nodes[i].ID
- mutation.done = true
if specs[i].ID.Value != nil {
id := specs[i].ID.Value.(int64)
nodes[i].ID = int(id)
}
+ mutation.done = true
return nodes[i], nil
})
for i := len(builder.hooks) - 1; i >= 0; i-- {
@@ -266,7 +231,7 @@ func (ptcb *PasswordTokenCreateBulk) Save(ctx context.Context) ([]*PasswordToken
}(i, ctx)
}
if len(mutators) > 0 {
- if _, err := mutators[0].Mutate(ctx, ptcb.builders[0].mutation); err != nil {
+ if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil {
return nil, err
}
}
@@ -274,8 +239,8 @@ func (ptcb *PasswordTokenCreateBulk) Save(ctx context.Context) ([]*PasswordToken
}
// SaveX is like Save, but panics if an error occurs.
-func (ptcb *PasswordTokenCreateBulk) SaveX(ctx context.Context) []*PasswordToken {
- v, err := ptcb.Save(ctx)
+func (_c *PasswordTokenCreateBulk) SaveX(ctx context.Context) []*PasswordToken {
+ v, err := _c.Save(ctx)
if err != nil {
panic(err)
}
@@ -283,14 +248,14 @@ func (ptcb *PasswordTokenCreateBulk) SaveX(ctx context.Context) []*PasswordToken
}
// Exec executes the query.
-func (ptcb *PasswordTokenCreateBulk) Exec(ctx context.Context) error {
- _, err := ptcb.Save(ctx)
+func (_c *PasswordTokenCreateBulk) Exec(ctx context.Context) error {
+ _, err := _c.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
-func (ptcb *PasswordTokenCreateBulk) ExecX(ctx context.Context) {
- if err := ptcb.Exec(ctx); err != nil {
+func (_c *PasswordTokenCreateBulk) ExecX(ctx context.Context) {
+ if err := _c.Exec(ctx); err != nil {
panic(err)
}
}
diff --git a/ent/passwordtoken_delete.go b/ent/passwordtoken_delete.go
index 6a9ee38..252f195 100644
--- a/ent/passwordtoken_delete.go
+++ b/ent/passwordtoken_delete.go
@@ -1,16 +1,15 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
- "fmt"
- "goweb/ent/passwordtoken"
- "goweb/ent/predicate"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/predicate"
)
// PasswordTokenDelete is the builder for deleting a PasswordToken entity.
@@ -21,80 +20,56 @@ type PasswordTokenDelete struct {
}
// Where appends a list predicates to the PasswordTokenDelete builder.
-func (ptd *PasswordTokenDelete) Where(ps ...predicate.PasswordToken) *PasswordTokenDelete {
- ptd.mutation.Where(ps...)
- return ptd
+func (_d *PasswordTokenDelete) Where(ps ...predicate.PasswordToken) *PasswordTokenDelete {
+ _d.mutation.Where(ps...)
+ return _d
}
// Exec executes the deletion query and returns how many vertices were deleted.
-func (ptd *PasswordTokenDelete) Exec(ctx context.Context) (int, error) {
- var (
- err error
- affected int
- )
- if len(ptd.hooks) == 0 {
- affected, err = ptd.sqlExec(ctx)
- } else {
- var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
- mutation, ok := m.(*PasswordTokenMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T", m)
- }
- ptd.mutation = mutation
- affected, err = ptd.sqlExec(ctx)
- mutation.done = true
- return affected, err
- })
- for i := len(ptd.hooks) - 1; i >= 0; i-- {
- if ptd.hooks[i] == nil {
- return 0, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)")
- }
- mut = ptd.hooks[i](mut)
- }
- if _, err := mut.Mutate(ctx, ptd.mutation); err != nil {
- return 0, err
- }
- }
- return affected, err
+func (_d *PasswordTokenDelete) Exec(ctx context.Context) (int, error) {
+ return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
}
// ExecX is like Exec, but panics if an error occurs.
-func (ptd *PasswordTokenDelete) ExecX(ctx context.Context) int {
- n, err := ptd.Exec(ctx)
+func (_d *PasswordTokenDelete) ExecX(ctx context.Context) int {
+ n, err := _d.Exec(ctx)
if err != nil {
panic(err)
}
return n
}
-func (ptd *PasswordTokenDelete) sqlExec(ctx context.Context) (int, error) {
- _spec := &sqlgraph.DeleteSpec{
- Node: &sqlgraph.NodeSpec{
- Table: passwordtoken.Table,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
- },
- }
- if ps := ptd.mutation.predicates; len(ps) > 0 {
+func (_d *PasswordTokenDelete) sqlExec(ctx context.Context) (int, error) {
+ _spec := sqlgraph.NewDeleteSpec(passwordtoken.Table, sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt))
+ if ps := _d.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
- return sqlgraph.DeleteNodes(ctx, ptd.driver, _spec)
+ affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
+ if err != nil && sqlgraph.IsConstraintError(err) {
+ err = &ConstraintError{msg: err.Error(), wrap: err}
+ }
+ _d.mutation.done = true
+ return affected, err
}
// PasswordTokenDeleteOne is the builder for deleting a single PasswordToken entity.
type PasswordTokenDeleteOne struct {
- ptd *PasswordTokenDelete
+ _d *PasswordTokenDelete
+}
+
+// Where appends a list predicates to the PasswordTokenDelete builder.
+func (_d *PasswordTokenDeleteOne) Where(ps ...predicate.PasswordToken) *PasswordTokenDeleteOne {
+ _d._d.mutation.Where(ps...)
+ return _d
}
// Exec executes the deletion query.
-func (ptdo *PasswordTokenDeleteOne) Exec(ctx context.Context) error {
- n, err := ptdo.ptd.Exec(ctx)
+func (_d *PasswordTokenDeleteOne) Exec(ctx context.Context) error {
+ n, err := _d._d.Exec(ctx)
switch {
case err != nil:
return err
@@ -106,6 +81,8 @@ func (ptdo *PasswordTokenDeleteOne) Exec(ctx context.Context) error {
}
// ExecX is like Exec, but panics if an error occurs.
-func (ptdo *PasswordTokenDeleteOne) ExecX(ctx context.Context) {
- ptdo.ptd.ExecX(ctx)
+func (_d *PasswordTokenDeleteOne) ExecX(ctx context.Context) {
+ if err := _d.Exec(ctx); err != nil {
+ panic(err)
+ }
}
diff --git a/ent/passwordtoken_query.go b/ent/passwordtoken_query.go
index 1e34e6a..8168b61 100644
--- a/ent/passwordtoken_query.go
+++ b/ent/passwordtoken_query.go
@@ -1,77 +1,73 @@
-// 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"
"math"
+ "entgo.io/ent"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/predicate"
+ "github.com/mikestefanello/pagoda/ent/user"
)
// PasswordTokenQuery is the builder for querying PasswordToken entities.
type PasswordTokenQuery struct {
config
- limit *int
- offset *int
- unique *bool
- order []OrderFunc
- fields []string
+ ctx *QueryContext
+ order []passwordtoken.OrderOption
+ inters []Interceptor
predicates []predicate.PasswordToken
- // eager-loading edges.
- withUser *UserQuery
- withFKs bool
+ withUser *UserQuery
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
}
// Where adds a new predicate for the PasswordTokenQuery builder.
-func (ptq *PasswordTokenQuery) Where(ps ...predicate.PasswordToken) *PasswordTokenQuery {
- ptq.predicates = append(ptq.predicates, ps...)
- return ptq
+func (_q *PasswordTokenQuery) Where(ps ...predicate.PasswordToken) *PasswordTokenQuery {
+ _q.predicates = append(_q.predicates, ps...)
+ return _q
}
-// Limit adds a limit step to the query.
-func (ptq *PasswordTokenQuery) Limit(limit int) *PasswordTokenQuery {
- ptq.limit = &limit
- return ptq
+// Limit the number of records to be returned by this query.
+func (_q *PasswordTokenQuery) Limit(limit int) *PasswordTokenQuery {
+ _q.ctx.Limit = &limit
+ return _q
}
-// Offset adds an offset step to the query.
-func (ptq *PasswordTokenQuery) Offset(offset int) *PasswordTokenQuery {
- ptq.offset = &offset
- return ptq
+// Offset to start from.
+func (_q *PasswordTokenQuery) Offset(offset int) *PasswordTokenQuery {
+ _q.ctx.Offset = &offset
+ return _q
}
// Unique configures the query builder to filter duplicate records on query.
// By default, unique is set to true, and can be disabled using this method.
-func (ptq *PasswordTokenQuery) Unique(unique bool) *PasswordTokenQuery {
- ptq.unique = &unique
- return ptq
+func (_q *PasswordTokenQuery) Unique(unique bool) *PasswordTokenQuery {
+ _q.ctx.Unique = &unique
+ return _q
}
-// Order adds an order step to the query.
-func (ptq *PasswordTokenQuery) Order(o ...OrderFunc) *PasswordTokenQuery {
- ptq.order = append(ptq.order, o...)
- return ptq
+// Order specifies how the records should be ordered.
+func (_q *PasswordTokenQuery) Order(o ...passwordtoken.OrderOption) *PasswordTokenQuery {
+ _q.order = append(_q.order, o...)
+ return _q
}
// QueryUser chains the current query on the "user" edge.
-func (ptq *PasswordTokenQuery) QueryUser() *UserQuery {
- query := &UserQuery{config: ptq.config}
+func (_q *PasswordTokenQuery) QueryUser() *UserQuery {
+ query := (&UserClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
- if err := ptq.prepareQuery(ctx); err != nil {
+ if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
- selector := ptq.sqlQuery(ctx)
+ selector := _q.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
@@ -80,7 +76,7 @@ func (ptq *PasswordTokenQuery) QueryUser() *UserQuery {
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2O, false, passwordtoken.UserTable, passwordtoken.UserColumn),
)
- fromU = sqlgraph.SetNeighbors(ptq.driver.Dialect(), step)
+ fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
}
return query
@@ -88,8 +84,8 @@ func (ptq *PasswordTokenQuery) QueryUser() *UserQuery {
// First returns the first PasswordToken entity from the query.
// Returns a *NotFoundError when no PasswordToken was found.
-func (ptq *PasswordTokenQuery) First(ctx context.Context) (*PasswordToken, error) {
- nodes, err := ptq.Limit(1).All(ctx)
+func (_q *PasswordTokenQuery) First(ctx context.Context) (*PasswordToken, error) {
+ nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst))
if err != nil {
return nil, err
}
@@ -100,8 +96,8 @@ func (ptq *PasswordTokenQuery) First(ctx context.Context) (*PasswordToken, error
}
// FirstX is like First, but panics if an error occurs.
-func (ptq *PasswordTokenQuery) FirstX(ctx context.Context) *PasswordToken {
- node, err := ptq.First(ctx)
+func (_q *PasswordTokenQuery) FirstX(ctx context.Context) *PasswordToken {
+ node, err := _q.First(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
@@ -110,9 +106,9 @@ func (ptq *PasswordTokenQuery) FirstX(ctx context.Context) *PasswordToken {
// FirstID returns the first PasswordToken ID from the query.
// Returns a *NotFoundError when no PasswordToken ID was found.
-func (ptq *PasswordTokenQuery) FirstID(ctx context.Context) (id int, err error) {
+func (_q *PasswordTokenQuery) FirstID(ctx context.Context) (id int, err error) {
var ids []int
- if ids, err = ptq.Limit(1).IDs(ctx); err != nil {
+ if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil {
return
}
if len(ids) == 0 {
@@ -123,8 +119,8 @@ func (ptq *PasswordTokenQuery) FirstID(ctx context.Context) (id int, err error)
}
// FirstIDX is like FirstID, but panics if an error occurs.
-func (ptq *PasswordTokenQuery) FirstIDX(ctx context.Context) int {
- id, err := ptq.FirstID(ctx)
+func (_q *PasswordTokenQuery) FirstIDX(ctx context.Context) int {
+ id, err := _q.FirstID(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
@@ -132,10 +128,10 @@ func (ptq *PasswordTokenQuery) FirstIDX(ctx context.Context) int {
}
// Only returns a single PasswordToken entity found by the query, ensuring it only returns one.
-// Returns a *NotSingularError when exactly one PasswordToken entity is not found.
+// Returns a *NotSingularError when more than one PasswordToken entity is found.
// Returns a *NotFoundError when no PasswordToken entities are found.
-func (ptq *PasswordTokenQuery) Only(ctx context.Context) (*PasswordToken, error) {
- nodes, err := ptq.Limit(2).All(ctx)
+func (_q *PasswordTokenQuery) Only(ctx context.Context) (*PasswordToken, error) {
+ nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly))
if err != nil {
return nil, err
}
@@ -150,8 +146,8 @@ func (ptq *PasswordTokenQuery) Only(ctx context.Context) (*PasswordToken, error)
}
// OnlyX is like Only, but panics if an error occurs.
-func (ptq *PasswordTokenQuery) OnlyX(ctx context.Context) *PasswordToken {
- node, err := ptq.Only(ctx)
+func (_q *PasswordTokenQuery) OnlyX(ctx context.Context) *PasswordToken {
+ node, err := _q.Only(ctx)
if err != nil {
panic(err)
}
@@ -159,11 +155,11 @@ func (ptq *PasswordTokenQuery) OnlyX(ctx context.Context) *PasswordToken {
}
// OnlyID is like Only, but returns the only PasswordToken ID in the query.
-// Returns a *NotSingularError when exactly one PasswordToken ID is not found.
+// Returns a *NotSingularError when more than one PasswordToken ID is found.
// Returns a *NotFoundError when no entities are found.
-func (ptq *PasswordTokenQuery) OnlyID(ctx context.Context) (id int, err error) {
+func (_q *PasswordTokenQuery) OnlyID(ctx context.Context) (id int, err error) {
var ids []int
- if ids, err = ptq.Limit(2).IDs(ctx); err != nil {
+ if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil {
return
}
switch len(ids) {
@@ -178,8 +174,8 @@ func (ptq *PasswordTokenQuery) OnlyID(ctx context.Context) (id int, err error) {
}
// OnlyIDX is like OnlyID, but panics if an error occurs.
-func (ptq *PasswordTokenQuery) OnlyIDX(ctx context.Context) int {
- id, err := ptq.OnlyID(ctx)
+func (_q *PasswordTokenQuery) OnlyIDX(ctx context.Context) int {
+ id, err := _q.OnlyID(ctx)
if err != nil {
panic(err)
}
@@ -187,16 +183,18 @@ func (ptq *PasswordTokenQuery) OnlyIDX(ctx context.Context) int {
}
// All executes the query and returns a list of PasswordTokens.
-func (ptq *PasswordTokenQuery) All(ctx context.Context) ([]*PasswordToken, error) {
- if err := ptq.prepareQuery(ctx); err != nil {
+func (_q *PasswordTokenQuery) All(ctx context.Context) ([]*PasswordToken, error) {
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll)
+ if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
- return ptq.sqlAll(ctx)
+ qr := querierAll[[]*PasswordToken, *PasswordTokenQuery]()
+ return withInterceptors[[]*PasswordToken](ctx, _q, qr, _q.inters)
}
// AllX is like All, but panics if an error occurs.
-func (ptq *PasswordTokenQuery) AllX(ctx context.Context) []*PasswordToken {
- nodes, err := ptq.All(ctx)
+func (_q *PasswordTokenQuery) AllX(ctx context.Context) []*PasswordToken {
+ nodes, err := _q.All(ctx)
if err != nil {
panic(err)
}
@@ -204,17 +202,20 @@ func (ptq *PasswordTokenQuery) AllX(ctx context.Context) []*PasswordToken {
}
// IDs executes the query and returns a list of PasswordToken IDs.
-func (ptq *PasswordTokenQuery) IDs(ctx context.Context) ([]int, error) {
- var ids []int
- if err := ptq.Select(passwordtoken.FieldID).Scan(ctx, &ids); err != nil {
+func (_q *PasswordTokenQuery) IDs(ctx context.Context) (ids []int, err error) {
+ if _q.ctx.Unique == nil && _q.path != nil {
+ _q.Unique(true)
+ }
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs)
+ if err = _q.Select(passwordtoken.FieldID).Scan(ctx, &ids); err != nil {
return nil, err
}
return ids, nil
}
// IDsX is like IDs, but panics if an error occurs.
-func (ptq *PasswordTokenQuery) IDsX(ctx context.Context) []int {
- ids, err := ptq.IDs(ctx)
+func (_q *PasswordTokenQuery) IDsX(ctx context.Context) []int {
+ ids, err := _q.IDs(ctx)
if err != nil {
panic(err)
}
@@ -222,16 +223,17 @@ func (ptq *PasswordTokenQuery) IDsX(ctx context.Context) []int {
}
// Count returns the count of the given query.
-func (ptq *PasswordTokenQuery) Count(ctx context.Context) (int, error) {
- if err := ptq.prepareQuery(ctx); err != nil {
+func (_q *PasswordTokenQuery) Count(ctx context.Context) (int, error) {
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount)
+ if err := _q.prepareQuery(ctx); err != nil {
return 0, err
}
- return ptq.sqlCount(ctx)
+ return withInterceptors[int](ctx, _q, querierCount[*PasswordTokenQuery](), _q.inters)
}
// CountX is like Count, but panics if an error occurs.
-func (ptq *PasswordTokenQuery) CountX(ctx context.Context) int {
- count, err := ptq.Count(ctx)
+func (_q *PasswordTokenQuery) CountX(ctx context.Context) int {
+ count, err := _q.Count(ctx)
if err != nil {
panic(err)
}
@@ -239,16 +241,21 @@ func (ptq *PasswordTokenQuery) CountX(ctx context.Context) int {
}
// Exist returns true if the query has elements in the graph.
-func (ptq *PasswordTokenQuery) Exist(ctx context.Context) (bool, error) {
- if err := ptq.prepareQuery(ctx); err != nil {
- return false, err
+func (_q *PasswordTokenQuery) Exist(ctx context.Context) (bool, error) {
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist)
+ switch _, err := _q.FirstID(ctx); {
+ case IsNotFound(err):
+ return false, nil
+ case err != nil:
+ return false, fmt.Errorf("ent: check existence: %w", err)
+ default:
+ return true, nil
}
- return ptq.sqlExist(ctx)
}
// ExistX is like Exist, but panics if an error occurs.
-func (ptq *PasswordTokenQuery) ExistX(ctx context.Context) bool {
- exist, err := ptq.Exist(ctx)
+func (_q *PasswordTokenQuery) ExistX(ctx context.Context) bool {
+ exist, err := _q.Exist(ctx)
if err != nil {
panic(err)
}
@@ -257,32 +264,32 @@ func (ptq *PasswordTokenQuery) ExistX(ctx context.Context) bool {
// Clone returns a duplicate of the PasswordTokenQuery builder, including all associated steps. It can be
// used to prepare common query builders and use them differently after the clone is made.
-func (ptq *PasswordTokenQuery) Clone() *PasswordTokenQuery {
- if ptq == nil {
+func (_q *PasswordTokenQuery) Clone() *PasswordTokenQuery {
+ if _q == nil {
return nil
}
return &PasswordTokenQuery{
- config: ptq.config,
- limit: ptq.limit,
- offset: ptq.offset,
- order: append([]OrderFunc{}, ptq.order...),
- predicates: append([]predicate.PasswordToken{}, ptq.predicates...),
- withUser: ptq.withUser.Clone(),
+ config: _q.config,
+ ctx: _q.ctx.Clone(),
+ order: append([]passwordtoken.OrderOption{}, _q.order...),
+ inters: append([]Interceptor{}, _q.inters...),
+ predicates: append([]predicate.PasswordToken{}, _q.predicates...),
+ withUser: _q.withUser.Clone(),
// clone intermediate query.
- sql: ptq.sql.Clone(),
- path: ptq.path,
+ sql: _q.sql.Clone(),
+ path: _q.path,
}
}
// WithUser tells the query-builder to eager-load the nodes that are connected to
// the "user" edge. The optional arguments are used to configure the query builder of the edge.
-func (ptq *PasswordTokenQuery) WithUser(opts ...func(*UserQuery)) *PasswordTokenQuery {
- query := &UserQuery{config: ptq.config}
+func (_q *PasswordTokenQuery) WithUser(opts ...func(*UserQuery)) *PasswordTokenQuery {
+ query := (&UserClient{config: _q.config}).Query()
for _, opt := range opts {
opt(query)
}
- ptq.withUser = query
- return ptq
+ _q.withUser = query
+ return _q
}
// GroupBy is used to group vertices by one or more fields/columns.
@@ -291,25 +298,21 @@ func (ptq *PasswordTokenQuery) WithUser(opts ...func(*UserQuery)) *PasswordToken
// Example:
//
// var v []struct {
-// Hash string `json:"hash,omitempty"`
+// Token string `json:"token,omitempty"`
// Count int `json:"count,omitempty"`
// }
//
// client.PasswordToken.Query().
-// GroupBy(passwordtoken.FieldHash).
+// GroupBy(passwordtoken.FieldToken).
// Aggregate(ent.Count()).
// Scan(ctx, &v)
-//
-func (ptq *PasswordTokenQuery) GroupBy(field string, fields ...string) *PasswordTokenGroupBy {
- group := &PasswordTokenGroupBy{config: ptq.config}
- group.fields = append([]string{field}, fields...)
- group.path = func(ctx context.Context) (prev *sql.Selector, err error) {
- if err := ptq.prepareQuery(ctx); err != nil {
- return nil, err
- }
- return ptq.sqlQuery(ctx), nil
- }
- return group
+func (_q *PasswordTokenQuery) GroupBy(field string, fields ...string) *PasswordTokenGroupBy {
+ _q.ctx.Fields = append([]string{field}, fields...)
+ grbuild := &PasswordTokenGroupBy{build: _q}
+ grbuild.flds = &_q.ctx.Fields
+ grbuild.label = passwordtoken.Label
+ grbuild.scan = grbuild.Scan
+ return grbuild
}
// Select allows the selection one or more fields/columns for the given query,
@@ -318,131 +321,134 @@ func (ptq *PasswordTokenQuery) GroupBy(field string, fields ...string) *Password
// Example:
//
// var v []struct {
-// Hash string `json:"hash,omitempty"`
+// Token string `json:"token,omitempty"`
// }
//
// client.PasswordToken.Query().
-// Select(passwordtoken.FieldHash).
+// Select(passwordtoken.FieldToken).
// Scan(ctx, &v)
-//
-func (ptq *PasswordTokenQuery) Select(fields ...string) *PasswordTokenSelect {
- ptq.fields = append(ptq.fields, fields...)
- return &PasswordTokenSelect{PasswordTokenQuery: ptq}
+func (_q *PasswordTokenQuery) Select(fields ...string) *PasswordTokenSelect {
+ _q.ctx.Fields = append(_q.ctx.Fields, fields...)
+ sbuild := &PasswordTokenSelect{PasswordTokenQuery: _q}
+ sbuild.label = passwordtoken.Label
+ sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan
+ return sbuild
}
-func (ptq *PasswordTokenQuery) prepareQuery(ctx context.Context) error {
- for _, f := range ptq.fields {
+// Aggregate returns a PasswordTokenSelect configured with the given aggregations.
+func (_q *PasswordTokenQuery) Aggregate(fns ...AggregateFunc) *PasswordTokenSelect {
+ return _q.Select().Aggregate(fns...)
+}
+
+func (_q *PasswordTokenQuery) prepareQuery(ctx context.Context) error {
+ for _, inter := range _q.inters {
+ if inter == nil {
+ return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
+ }
+ if trv, ok := inter.(Traverser); ok {
+ if err := trv.Traverse(ctx, _q); err != nil {
+ return err
+ }
+ }
+ }
+ for _, f := range _q.ctx.Fields {
if !passwordtoken.ValidColumn(f) {
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
}
- if ptq.path != nil {
- prev, err := ptq.path(ctx)
+ if _q.path != nil {
+ prev, err := _q.path(ctx)
if err != nil {
return err
}
- ptq.sql = prev
+ _q.sql = prev
}
return nil
}
-func (ptq *PasswordTokenQuery) sqlAll(ctx context.Context) ([]*PasswordToken, error) {
+func (_q *PasswordTokenQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*PasswordToken, error) {
var (
nodes = []*PasswordToken{}
- withFKs = ptq.withFKs
- _spec = ptq.querySpec()
+ _spec = _q.querySpec()
loadedTypes = [1]bool{
- ptq.withUser != nil,
+ _q.withUser != nil,
}
)
- if ptq.withUser != nil {
- withFKs = true
+ _spec.ScanValues = func(columns []string) ([]any, error) {
+ return (*PasswordToken).scanValues(nil, columns)
}
- if withFKs {
- _spec.Node.Columns = append(_spec.Node.Columns, passwordtoken.ForeignKeys...)
- }
- _spec.ScanValues = func(columns []string) ([]interface{}, error) {
- node := &PasswordToken{config: ptq.config}
+ _spec.Assign = func(columns []string, values []any) error {
+ node := &PasswordToken{config: _q.config}
nodes = append(nodes, node)
- return node.scanValues(columns)
- }
- _spec.Assign = func(columns []string, values []interface{}) error {
- if len(nodes) == 0 {
- return fmt.Errorf("ent: Assign called without calling ScanValues")
- }
- node := nodes[len(nodes)-1]
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
- if err := sqlgraph.QueryNodes(ctx, ptq.driver, _spec); err != nil {
+ for i := range hooks {
+ hooks[i](ctx, _spec)
+ }
+ if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil {
return nil, err
}
if len(nodes) == 0 {
return nodes, nil
}
-
- if query := ptq.withUser; query != nil {
- ids := make([]int, 0, len(nodes))
- nodeids := make(map[int][]*PasswordToken)
- for i := range nodes {
- if nodes[i].password_token_user == nil {
- continue
- }
- fk := *nodes[i].password_token_user
- if _, ok := nodeids[fk]; !ok {
- ids = append(ids, fk)
- }
- nodeids[fk] = append(nodeids[fk], nodes[i])
- }
- query.Where(user.IDIn(ids...))
- neighbors, err := query.All(ctx)
- if err != nil {
+ if query := _q.withUser; query != nil {
+ if err := _q.loadUser(ctx, query, nodes, nil,
+ func(n *PasswordToken, e *User) { n.Edges.User = e }); err != nil {
return nil, err
}
- for _, n := range neighbors {
- nodes, ok := nodeids[n.ID]
- if !ok {
- return nil, fmt.Errorf(`unexpected foreign-key "password_token_user" returned %v`, n.ID)
- }
- for i := range nodes {
- nodes[i].Edges.User = n
- }
- }
}
-
return nodes, nil
}
-func (ptq *PasswordTokenQuery) sqlCount(ctx context.Context) (int, error) {
- _spec := ptq.querySpec()
- return sqlgraph.CountNodes(ctx, ptq.driver, _spec)
-}
-
-func (ptq *PasswordTokenQuery) sqlExist(ctx context.Context) (bool, error) {
- n, err := ptq.sqlCount(ctx)
+func (_q *PasswordTokenQuery) loadUser(ctx context.Context, query *UserQuery, nodes []*PasswordToken, init func(*PasswordToken), assign func(*PasswordToken, *User)) error {
+ ids := make([]int, 0, len(nodes))
+ nodeids := make(map[int][]*PasswordToken)
+ for i := range nodes {
+ fk := nodes[i].UserID
+ if _, ok := nodeids[fk]; !ok {
+ ids = append(ids, fk)
+ }
+ nodeids[fk] = append(nodeids[fk], nodes[i])
+ }
+ if len(ids) == 0 {
+ return nil
+ }
+ query.Where(user.IDIn(ids...))
+ neighbors, err := query.All(ctx)
if err != nil {
- return false, fmt.Errorf("ent: check existence: %w", err)
+ return err
}
- return n > 0, nil
+ for _, n := range neighbors {
+ nodes, ok := nodeids[n.ID]
+ if !ok {
+ return fmt.Errorf(`unexpected foreign-key "user_id" returned %v`, n.ID)
+ }
+ for i := range nodes {
+ assign(nodes[i], n)
+ }
+ }
+ return nil
}
-func (ptq *PasswordTokenQuery) querySpec() *sqlgraph.QuerySpec {
- _spec := &sqlgraph.QuerySpec{
- Node: &sqlgraph.NodeSpec{
- Table: passwordtoken.Table,
- Columns: passwordtoken.Columns,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
- },
- From: ptq.sql,
- Unique: true,
+func (_q *PasswordTokenQuery) sqlCount(ctx context.Context) (int, error) {
+ _spec := _q.querySpec()
+ _spec.Node.Columns = _q.ctx.Fields
+ if len(_q.ctx.Fields) > 0 {
+ _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
}
- if unique := ptq.unique; unique != nil {
+ return sqlgraph.CountNodes(ctx, _q.driver, _spec)
+}
+
+func (_q *PasswordTokenQuery) querySpec() *sqlgraph.QuerySpec {
+ _spec := sqlgraph.NewQuerySpec(passwordtoken.Table, passwordtoken.Columns, sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt))
+ _spec.From = _q.sql
+ if unique := _q.ctx.Unique; unique != nil {
_spec.Unique = *unique
+ } else if _q.path != nil {
+ _spec.Unique = true
}
- if fields := ptq.fields; len(fields) > 0 {
+ if fields := _q.ctx.Fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, passwordtoken.FieldID)
for i := range fields {
@@ -450,21 +456,24 @@ func (ptq *PasswordTokenQuery) querySpec() *sqlgraph.QuerySpec {
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
}
}
+ if _q.withUser != nil {
+ _spec.Node.AddColumnOnce(passwordtoken.FieldUserID)
+ }
}
- if ps := ptq.predicates; len(ps) > 0 {
+ if ps := _q.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
- if limit := ptq.limit; limit != nil {
+ if limit := _q.ctx.Limit; limit != nil {
_spec.Limit = *limit
}
- if offset := ptq.offset; offset != nil {
+ if offset := _q.ctx.Offset; offset != nil {
_spec.Offset = *offset
}
- if ps := ptq.order; len(ps) > 0 {
+ if ps := _q.order; len(ps) > 0 {
_spec.Order = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
@@ -474,30 +483,33 @@ func (ptq *PasswordTokenQuery) querySpec() *sqlgraph.QuerySpec {
return _spec
}
-func (ptq *PasswordTokenQuery) sqlQuery(ctx context.Context) *sql.Selector {
- builder := sql.Dialect(ptq.driver.Dialect())
+func (_q *PasswordTokenQuery) sqlQuery(ctx context.Context) *sql.Selector {
+ builder := sql.Dialect(_q.driver.Dialect())
t1 := builder.Table(passwordtoken.Table)
- columns := ptq.fields
+ columns := _q.ctx.Fields
if len(columns) == 0 {
columns = passwordtoken.Columns
}
selector := builder.Select(t1.Columns(columns...)...).From(t1)
- if ptq.sql != nil {
- selector = ptq.sql
+ if _q.sql != nil {
+ selector = _q.sql
selector.Select(selector.Columns(columns...)...)
}
- for _, p := range ptq.predicates {
+ if _q.ctx.Unique != nil && *_q.ctx.Unique {
+ selector.Distinct()
+ }
+ for _, p := range _q.predicates {
p(selector)
}
- for _, p := range ptq.order {
+ for _, p := range _q.order {
p(selector)
}
- if offset := ptq.offset; offset != nil {
+ if offset := _q.ctx.Offset; offset != nil {
// limit is mandatory for offset clause. We start
// with default value, and override it below if needed.
selector.Offset(*offset).Limit(math.MaxInt32)
}
- if limit := ptq.limit; limit != nil {
+ if limit := _q.ctx.Limit; limit != nil {
selector.Limit(*limit)
}
return selector
@@ -505,488 +517,88 @@ func (ptq *PasswordTokenQuery) sqlQuery(ctx context.Context) *sql.Selector {
// PasswordTokenGroupBy is the group-by builder for PasswordToken entities.
type PasswordTokenGroupBy struct {
- config
- fields []string
- fns []AggregateFunc
- // intermediate query (i.e. traversal path).
- sql *sql.Selector
- path func(context.Context) (*sql.Selector, error)
+ selector
+ build *PasswordTokenQuery
}
// Aggregate adds the given aggregation functions to the group-by query.
-func (ptgb *PasswordTokenGroupBy) Aggregate(fns ...AggregateFunc) *PasswordTokenGroupBy {
- ptgb.fns = append(ptgb.fns, fns...)
- return ptgb
+func (_g *PasswordTokenGroupBy) Aggregate(fns ...AggregateFunc) *PasswordTokenGroupBy {
+ _g.fns = append(_g.fns, fns...)
+ return _g
}
-// Scan applies the group-by query and scans the result into the given value.
-func (ptgb *PasswordTokenGroupBy) Scan(ctx context.Context, v interface{}) error {
- query, err := ptgb.path(ctx)
- if err != nil {
+// Scan applies the selector query and scans the result into the given value.
+func (_g *PasswordTokenGroupBy) Scan(ctx context.Context, v any) error {
+ ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy)
+ if err := _g.build.prepareQuery(ctx); err != nil {
return err
}
- ptgb.sql = query
- return ptgb.sqlScan(ctx, v)
+ return scanWithInterceptors[*PasswordTokenQuery, *PasswordTokenGroupBy](ctx, _g.build, _g, _g.build.inters, v)
}
-// ScanX is like Scan, but panics if an error occurs.
-func (ptgb *PasswordTokenGroupBy) ScanX(ctx context.Context, v interface{}) {
- if err := ptgb.Scan(ctx, v); err != nil {
- panic(err)
+func (_g *PasswordTokenGroupBy) sqlScan(ctx context.Context, root *PasswordTokenQuery, v any) error {
+ selector := root.sqlQuery(ctx).Select()
+ aggregation := make([]string, 0, len(_g.fns))
+ for _, fn := range _g.fns {
+ aggregation = append(aggregation, fn(selector))
}
-}
-
-// Strings returns list of strings from group-by.
-// It is only allowed when executing a group-by query with one field.
-func (ptgb *PasswordTokenGroupBy) Strings(ctx context.Context) ([]string, error) {
- if len(ptgb.fields) > 1 {
- return nil, errors.New("ent: PasswordTokenGroupBy.Strings is not achievable when grouping more than 1 field")
- }
- var v []string
- if err := ptgb.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// StringsX is like Strings, but panics if an error occurs.
-func (ptgb *PasswordTokenGroupBy) StringsX(ctx context.Context) []string {
- v, err := ptgb.Strings(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// String returns a single string from a group-by query.
-// It is only allowed when executing a group-by query with one field.
-func (ptgb *PasswordTokenGroupBy) String(ctx context.Context) (_ string, err error) {
- var v []string
- if v, err = ptgb.Strings(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{passwordtoken.Label}
- default:
- err = fmt.Errorf("ent: PasswordTokenGroupBy.Strings returned %d results when one was expected", len(v))
- }
- return
-}
-
-// StringX is like String, but panics if an error occurs.
-func (ptgb *PasswordTokenGroupBy) StringX(ctx context.Context) string {
- v, err := ptgb.String(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Ints returns list of ints from group-by.
-// It is only allowed when executing a group-by query with one field.
-func (ptgb *PasswordTokenGroupBy) Ints(ctx context.Context) ([]int, error) {
- if len(ptgb.fields) > 1 {
- return nil, errors.New("ent: PasswordTokenGroupBy.Ints is not achievable when grouping more than 1 field")
- }
- var v []int
- if err := ptgb.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// IntsX is like Ints, but panics if an error occurs.
-func (ptgb *PasswordTokenGroupBy) IntsX(ctx context.Context) []int {
- v, err := ptgb.Ints(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Int returns a single int from a group-by query.
-// It is only allowed when executing a group-by query with one field.
-func (ptgb *PasswordTokenGroupBy) Int(ctx context.Context) (_ int, err error) {
- var v []int
- if v, err = ptgb.Ints(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{passwordtoken.Label}
- default:
- err = fmt.Errorf("ent: PasswordTokenGroupBy.Ints returned %d results when one was expected", len(v))
- }
- return
-}
-
-// IntX is like Int, but panics if an error occurs.
-func (ptgb *PasswordTokenGroupBy) IntX(ctx context.Context) int {
- v, err := ptgb.Int(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Float64s returns list of float64s from group-by.
-// It is only allowed when executing a group-by query with one field.
-func (ptgb *PasswordTokenGroupBy) Float64s(ctx context.Context) ([]float64, error) {
- if len(ptgb.fields) > 1 {
- return nil, errors.New("ent: PasswordTokenGroupBy.Float64s is not achievable when grouping more than 1 field")
- }
- var v []float64
- if err := ptgb.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// Float64sX is like Float64s, but panics if an error occurs.
-func (ptgb *PasswordTokenGroupBy) Float64sX(ctx context.Context) []float64 {
- v, err := ptgb.Float64s(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Float64 returns a single float64 from a group-by query.
-// It is only allowed when executing a group-by query with one field.
-func (ptgb *PasswordTokenGroupBy) Float64(ctx context.Context) (_ float64, err error) {
- var v []float64
- if v, err = ptgb.Float64s(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{passwordtoken.Label}
- default:
- err = fmt.Errorf("ent: PasswordTokenGroupBy.Float64s returned %d results when one was expected", len(v))
- }
- return
-}
-
-// Float64X is like Float64, but panics if an error occurs.
-func (ptgb *PasswordTokenGroupBy) Float64X(ctx context.Context) float64 {
- v, err := ptgb.Float64(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Bools returns list of bools from group-by.
-// It is only allowed when executing a group-by query with one field.
-func (ptgb *PasswordTokenGroupBy) Bools(ctx context.Context) ([]bool, error) {
- if len(ptgb.fields) > 1 {
- return nil, errors.New("ent: PasswordTokenGroupBy.Bools is not achievable when grouping more than 1 field")
- }
- var v []bool
- if err := ptgb.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// BoolsX is like Bools, but panics if an error occurs.
-func (ptgb *PasswordTokenGroupBy) BoolsX(ctx context.Context) []bool {
- v, err := ptgb.Bools(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Bool returns a single bool from a group-by query.
-// It is only allowed when executing a group-by query with one field.
-func (ptgb *PasswordTokenGroupBy) Bool(ctx context.Context) (_ bool, err error) {
- var v []bool
- if v, err = ptgb.Bools(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{passwordtoken.Label}
- default:
- err = fmt.Errorf("ent: PasswordTokenGroupBy.Bools returned %d results when one was expected", len(v))
- }
- return
-}
-
-// BoolX is like Bool, but panics if an error occurs.
-func (ptgb *PasswordTokenGroupBy) BoolX(ctx context.Context) bool {
- v, err := ptgb.Bool(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-func (ptgb *PasswordTokenGroupBy) sqlScan(ctx context.Context, v interface{}) error {
- for _, f := range ptgb.fields {
- if !passwordtoken.ValidColumn(f) {
- return &ValidationError{Name: f, err: fmt.Errorf("invalid field %q for group-by", f)}
+ if len(selector.SelectedColumns()) == 0 {
+ columns := make([]string, 0, len(*_g.flds)+len(_g.fns))
+ for _, f := range *_g.flds {
+ columns = append(columns, selector.C(f))
}
+ columns = append(columns, aggregation...)
+ selector.Select(columns...)
}
- selector := ptgb.sqlQuery()
+ selector.GroupBy(selector.Columns(*_g.flds...)...)
if err := selector.Err(); err != nil {
return err
}
rows := &sql.Rows{}
query, args := selector.Query()
- if err := ptgb.driver.Query(ctx, query, args, rows); err != nil {
+ if err := _g.build.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}
-func (ptgb *PasswordTokenGroupBy) sqlQuery() *sql.Selector {
- selector := ptgb.sql.Select()
- aggregation := make([]string, 0, len(ptgb.fns))
- for _, fn := range ptgb.fns {
- aggregation = append(aggregation, fn(selector))
- }
- // If no columns were selected in a custom aggregation function, the default
- // selection is the fields used for "group-by", and the aggregation functions.
- if len(selector.SelectedColumns()) == 0 {
- columns := make([]string, 0, len(ptgb.fields)+len(ptgb.fns))
- for _, f := range ptgb.fields {
- columns = append(columns, selector.C(f))
- }
- for _, c := range aggregation {
- columns = append(columns, c)
- }
- selector.Select(columns...)
- }
- return selector.GroupBy(selector.Columns(ptgb.fields...)...)
-}
-
// PasswordTokenSelect is the builder for selecting fields of PasswordToken entities.
type PasswordTokenSelect struct {
*PasswordTokenQuery
- // intermediate query (i.e. traversal path).
- sql *sql.Selector
+ selector
+}
+
+// Aggregate adds the given aggregation functions to the selector query.
+func (_s *PasswordTokenSelect) Aggregate(fns ...AggregateFunc) *PasswordTokenSelect {
+ _s.fns = append(_s.fns, fns...)
+ return _s
}
// Scan applies the selector query and scans the result into the given value.
-func (pts *PasswordTokenSelect) Scan(ctx context.Context, v interface{}) error {
- if err := pts.prepareQuery(ctx); err != nil {
+func (_s *PasswordTokenSelect) Scan(ctx context.Context, v any) error {
+ ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect)
+ if err := _s.prepareQuery(ctx); err != nil {
return err
}
- pts.sql = pts.PasswordTokenQuery.sqlQuery(ctx)
- return pts.sqlScan(ctx, v)
+ return scanWithInterceptors[*PasswordTokenQuery, *PasswordTokenSelect](ctx, _s.PasswordTokenQuery, _s, _s.inters, v)
}
-// ScanX is like Scan, but panics if an error occurs.
-func (pts *PasswordTokenSelect) ScanX(ctx context.Context, v interface{}) {
- if err := pts.Scan(ctx, v); err != nil {
- panic(err)
+func (_s *PasswordTokenSelect) sqlScan(ctx context.Context, root *PasswordTokenQuery, v any) error {
+ selector := root.sqlQuery(ctx)
+ aggregation := make([]string, 0, len(_s.fns))
+ for _, fn := range _s.fns {
+ aggregation = append(aggregation, fn(selector))
}
-}
-
-// Strings returns list of strings from a selector. It is only allowed when selecting one field.
-func (pts *PasswordTokenSelect) Strings(ctx context.Context) ([]string, error) {
- if len(pts.fields) > 1 {
- return nil, errors.New("ent: PasswordTokenSelect.Strings is not achievable when selecting more than 1 field")
+ switch n := len(*_s.selector.flds); {
+ case n == 0 && len(aggregation) > 0:
+ selector.Select(aggregation...)
+ case n != 0 && len(aggregation) > 0:
+ selector.AppendSelect(aggregation...)
}
- var v []string
- if err := pts.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// StringsX is like Strings, but panics if an error occurs.
-func (pts *PasswordTokenSelect) StringsX(ctx context.Context) []string {
- v, err := pts.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 (pts *PasswordTokenSelect) String(ctx context.Context) (_ string, err error) {
- var v []string
- if v, err = pts.Strings(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{passwordtoken.Label}
- default:
- err = fmt.Errorf("ent: PasswordTokenSelect.Strings returned %d results when one was expected", len(v))
- }
- return
-}
-
-// StringX is like String, but panics if an error occurs.
-func (pts *PasswordTokenSelect) StringX(ctx context.Context) string {
- v, err := pts.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 (pts *PasswordTokenSelect) Ints(ctx context.Context) ([]int, error) {
- if len(pts.fields) > 1 {
- return nil, errors.New("ent: PasswordTokenSelect.Ints is not achievable when selecting more than 1 field")
- }
- var v []int
- if err := pts.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// IntsX is like Ints, but panics if an error occurs.
-func (pts *PasswordTokenSelect) IntsX(ctx context.Context) []int {
- v, err := pts.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 (pts *PasswordTokenSelect) Int(ctx context.Context) (_ int, err error) {
- var v []int
- if v, err = pts.Ints(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{passwordtoken.Label}
- default:
- err = fmt.Errorf("ent: PasswordTokenSelect.Ints returned %d results when one was expected", len(v))
- }
- return
-}
-
-// IntX is like Int, but panics if an error occurs.
-func (pts *PasswordTokenSelect) IntX(ctx context.Context) int {
- v, err := pts.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 (pts *PasswordTokenSelect) Float64s(ctx context.Context) ([]float64, error) {
- if len(pts.fields) > 1 {
- return nil, errors.New("ent: PasswordTokenSelect.Float64s is not achievable when selecting more than 1 field")
- }
- var v []float64
- if err := pts.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// Float64sX is like Float64s, but panics if an error occurs.
-func (pts *PasswordTokenSelect) Float64sX(ctx context.Context) []float64 {
- v, err := pts.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 (pts *PasswordTokenSelect) Float64(ctx context.Context) (_ float64, err error) {
- var v []float64
- if v, err = pts.Float64s(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{passwordtoken.Label}
- default:
- err = fmt.Errorf("ent: PasswordTokenSelect.Float64s returned %d results when one was expected", len(v))
- }
- return
-}
-
-// Float64X is like Float64, but panics if an error occurs.
-func (pts *PasswordTokenSelect) Float64X(ctx context.Context) float64 {
- v, err := pts.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 (pts *PasswordTokenSelect) Bools(ctx context.Context) ([]bool, error) {
- if len(pts.fields) > 1 {
- return nil, errors.New("ent: PasswordTokenSelect.Bools is not achievable when selecting more than 1 field")
- }
- var v []bool
- if err := pts.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// BoolsX is like Bools, but panics if an error occurs.
-func (pts *PasswordTokenSelect) BoolsX(ctx context.Context) []bool {
- v, err := pts.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 (pts *PasswordTokenSelect) Bool(ctx context.Context) (_ bool, err error) {
- var v []bool
- if v, err = pts.Bools(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{passwordtoken.Label}
- default:
- err = fmt.Errorf("ent: PasswordTokenSelect.Bools returned %d results when one was expected", len(v))
- }
- return
-}
-
-// BoolX is like Bool, but panics if an error occurs.
-func (pts *PasswordTokenSelect) BoolX(ctx context.Context) bool {
- v, err := pts.Bool(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-func (pts *PasswordTokenSelect) sqlScan(ctx context.Context, v interface{}) error {
rows := &sql.Rows{}
- query, args := pts.sql.Query()
- if err := pts.driver.Query(ctx, query, args, rows); err != nil {
+ query, args := selector.Query()
+ if err := _s.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
diff --git a/ent/passwordtoken_update.go b/ent/passwordtoken_update.go
index 9dc5cad..de59b37 100644
--- a/ent/passwordtoken_update.go
+++ b/ent/passwordtoken_update.go
@@ -1,4 +1,4 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
@@ -6,14 +6,14 @@ import (
"context"
"errors"
"fmt"
- "goweb/ent/passwordtoken"
- "goweb/ent/predicate"
- "goweb/ent/user"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/predicate"
+ "github.com/mikestefanello/pagoda/ent/user"
)
// PasswordTokenUpdate is the builder for updating PasswordToken entities.
@@ -24,94 +24,77 @@ type PasswordTokenUpdate struct {
}
// Where appends a list predicates to the PasswordTokenUpdate builder.
-func (ptu *PasswordTokenUpdate) Where(ps ...predicate.PasswordToken) *PasswordTokenUpdate {
- ptu.mutation.Where(ps...)
- return ptu
+func (_u *PasswordTokenUpdate) Where(ps ...predicate.PasswordToken) *PasswordTokenUpdate {
+ _u.mutation.Where(ps...)
+ return _u
}
-// SetHash sets the "hash" field.
-func (ptu *PasswordTokenUpdate) SetHash(s string) *PasswordTokenUpdate {
- ptu.mutation.SetHash(s)
- return ptu
+// SetToken sets the "token" field.
+func (_u *PasswordTokenUpdate) SetToken(v string) *PasswordTokenUpdate {
+ _u.mutation.SetToken(v)
+ return _u
+}
+
+// SetNillableToken sets the "token" field if the given value is not nil.
+func (_u *PasswordTokenUpdate) SetNillableToken(v *string) *PasswordTokenUpdate {
+ if v != nil {
+ _u.SetToken(*v)
+ }
+ return _u
+}
+
+// SetUserID sets the "user_id" field.
+func (_u *PasswordTokenUpdate) SetUserID(v int) *PasswordTokenUpdate {
+ _u.mutation.SetUserID(v)
+ return _u
+}
+
+// SetNillableUserID sets the "user_id" field if the given value is not nil.
+func (_u *PasswordTokenUpdate) SetNillableUserID(v *int) *PasswordTokenUpdate {
+ if v != nil {
+ _u.SetUserID(*v)
+ }
+ return _u
}
// SetCreatedAt sets the "created_at" field.
-func (ptu *PasswordTokenUpdate) SetCreatedAt(t time.Time) *PasswordTokenUpdate {
- ptu.mutation.SetCreatedAt(t)
- return ptu
+func (_u *PasswordTokenUpdate) SetCreatedAt(v time.Time) *PasswordTokenUpdate {
+ _u.mutation.SetCreatedAt(v)
+ return _u
}
// SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
-func (ptu *PasswordTokenUpdate) SetNillableCreatedAt(t *time.Time) *PasswordTokenUpdate {
- if t != nil {
- ptu.SetCreatedAt(*t)
+func (_u *PasswordTokenUpdate) SetNillableCreatedAt(v *time.Time) *PasswordTokenUpdate {
+ if v != nil {
+ _u.SetCreatedAt(*v)
}
- return ptu
-}
-
-// SetUserID sets the "user" edge to the User entity by ID.
-func (ptu *PasswordTokenUpdate) SetUserID(id int) *PasswordTokenUpdate {
- ptu.mutation.SetUserID(id)
- return ptu
+ return _u
}
// SetUser sets the "user" edge to the User entity.
-func (ptu *PasswordTokenUpdate) SetUser(u *User) *PasswordTokenUpdate {
- return ptu.SetUserID(u.ID)
+func (_u *PasswordTokenUpdate) SetUser(v *User) *PasswordTokenUpdate {
+ return _u.SetUserID(v.ID)
}
// Mutation returns the PasswordTokenMutation object of the builder.
-func (ptu *PasswordTokenUpdate) Mutation() *PasswordTokenMutation {
- return ptu.mutation
+func (_u *PasswordTokenUpdate) Mutation() *PasswordTokenMutation {
+ return _u.mutation
}
// ClearUser clears the "user" edge to the User entity.
-func (ptu *PasswordTokenUpdate) ClearUser() *PasswordTokenUpdate {
- ptu.mutation.ClearUser()
- return ptu
+func (_u *PasswordTokenUpdate) ClearUser() *PasswordTokenUpdate {
+ _u.mutation.ClearUser()
+ return _u
}
// Save executes the query and returns the number of nodes affected by the update operation.
-func (ptu *PasswordTokenUpdate) Save(ctx context.Context) (int, error) {
- var (
- err error
- affected int
- )
- if len(ptu.hooks) == 0 {
- if err = ptu.check(); err != nil {
- return 0, err
- }
- affected, err = ptu.sqlSave(ctx)
- } else {
- var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
- mutation, ok := m.(*PasswordTokenMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T", m)
- }
- if err = ptu.check(); err != nil {
- return 0, err
- }
- ptu.mutation = mutation
- affected, err = ptu.sqlSave(ctx)
- mutation.done = true
- return affected, err
- })
- for i := len(ptu.hooks) - 1; i >= 0; i-- {
- if ptu.hooks[i] == nil {
- return 0, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)")
- }
- mut = ptu.hooks[i](mut)
- }
- if _, err := mut.Mutate(ctx, ptu.mutation); err != nil {
- return 0, err
- }
- }
- return affected, err
+func (_u *PasswordTokenUpdate) Save(ctx context.Context) (int, error) {
+ return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
}
// SaveX is like Save, but panics if an error occurs.
-func (ptu *PasswordTokenUpdate) SaveX(ctx context.Context) int {
- affected, err := ptu.Save(ctx)
+func (_u *PasswordTokenUpdate) SaveX(ctx context.Context) int {
+ affected, err := _u.Save(ctx)
if err != nil {
panic(err)
}
@@ -119,64 +102,50 @@ func (ptu *PasswordTokenUpdate) SaveX(ctx context.Context) int {
}
// Exec executes the query.
-func (ptu *PasswordTokenUpdate) Exec(ctx context.Context) error {
- _, err := ptu.Save(ctx)
+func (_u *PasswordTokenUpdate) Exec(ctx context.Context) error {
+ _, err := _u.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
-func (ptu *PasswordTokenUpdate) ExecX(ctx context.Context) {
- if err := ptu.Exec(ctx); err != nil {
+func (_u *PasswordTokenUpdate) ExecX(ctx context.Context) {
+ if err := _u.Exec(ctx); err != nil {
panic(err)
}
}
// check runs all checks and user-defined validators on the builder.
-func (ptu *PasswordTokenUpdate) check() error {
- if v, ok := ptu.mutation.Hash(); ok {
- if err := passwordtoken.HashValidator(v); err != nil {
- return &ValidationError{Name: "hash", err: fmt.Errorf("ent: validator failed for field \"hash\": %w", err)}
+func (_u *PasswordTokenUpdate) check() error {
+ if v, ok := _u.mutation.Token(); ok {
+ if err := passwordtoken.TokenValidator(v); err != nil {
+ return &ValidationError{Name: "token", err: fmt.Errorf(`ent: validator failed for field "PasswordToken.token": %w`, err)}
}
}
- if _, ok := ptu.mutation.UserID(); ptu.mutation.UserCleared() && !ok {
- return errors.New("ent: clearing a required unique edge \"user\"")
+ if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
+ return errors.New(`ent: clearing a required unique edge "PasswordToken.user"`)
}
return nil
}
-func (ptu *PasswordTokenUpdate) sqlSave(ctx context.Context) (n int, err error) {
- _spec := &sqlgraph.UpdateSpec{
- Node: &sqlgraph.NodeSpec{
- Table: passwordtoken.Table,
- Columns: passwordtoken.Columns,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
- },
+func (_u *PasswordTokenUpdate) sqlSave(ctx context.Context) (_node int, err error) {
+ if err := _u.check(); err != nil {
+ return _node, err
}
- if ps := ptu.mutation.predicates; len(ps) > 0 {
+ _spec := sqlgraph.NewUpdateSpec(passwordtoken.Table, passwordtoken.Columns, sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt))
+ if ps := _u.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
- if value, ok := ptu.mutation.Hash(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: passwordtoken.FieldHash,
- })
+ if value, ok := _u.mutation.Token(); ok {
+ _spec.SetField(passwordtoken.FieldToken, field.TypeString, value)
}
- if value, ok := ptu.mutation.CreatedAt(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeTime,
- Value: value,
- Column: passwordtoken.FieldCreatedAt,
- })
+ if value, ok := _u.mutation.CreatedAt(); ok {
+ _spec.SetField(passwordtoken.FieldCreatedAt, field.TypeTime, value)
}
- if ptu.mutation.UserCleared() {
+ if _u.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
@@ -184,15 +153,12 @@ func (ptu *PasswordTokenUpdate) sqlSave(ctx context.Context) (n int, err error)
Columns: []string{passwordtoken.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
- if nodes := ptu.mutation.UserIDs(); len(nodes) > 0 {
+ if nodes := _u.mutation.UserIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
@@ -200,10 +166,7 @@ func (ptu *PasswordTokenUpdate) sqlSave(ctx context.Context) (n int, err error)
Columns: []string{passwordtoken.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt),
},
}
for _, k := range nodes {
@@ -211,15 +174,16 @@ func (ptu *PasswordTokenUpdate) sqlSave(ctx context.Context) (n int, err error)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
- if n, err = sqlgraph.UpdateNodes(ctx, ptu.driver, _spec); err != nil {
+ if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{passwordtoken.Label}
} else if sqlgraph.IsConstraintError(err) {
- err = &ConstraintError{err.Error(), err}
+ err = &ConstraintError{msg: err.Error(), wrap: err}
}
return 0, err
}
- return n, nil
+ _u.mutation.done = true
+ return _node, nil
}
// PasswordTokenUpdateOne is the builder for updating a single PasswordToken entity.
@@ -230,96 +194,85 @@ type PasswordTokenUpdateOne struct {
mutation *PasswordTokenMutation
}
-// SetHash sets the "hash" field.
-func (ptuo *PasswordTokenUpdateOne) SetHash(s string) *PasswordTokenUpdateOne {
- ptuo.mutation.SetHash(s)
- return ptuo
+// SetToken sets the "token" field.
+func (_u *PasswordTokenUpdateOne) SetToken(v string) *PasswordTokenUpdateOne {
+ _u.mutation.SetToken(v)
+ return _u
+}
+
+// SetNillableToken sets the "token" field if the given value is not nil.
+func (_u *PasswordTokenUpdateOne) SetNillableToken(v *string) *PasswordTokenUpdateOne {
+ if v != nil {
+ _u.SetToken(*v)
+ }
+ return _u
+}
+
+// SetUserID sets the "user_id" field.
+func (_u *PasswordTokenUpdateOne) SetUserID(v int) *PasswordTokenUpdateOne {
+ _u.mutation.SetUserID(v)
+ return _u
+}
+
+// SetNillableUserID sets the "user_id" field if the given value is not nil.
+func (_u *PasswordTokenUpdateOne) SetNillableUserID(v *int) *PasswordTokenUpdateOne {
+ if v != nil {
+ _u.SetUserID(*v)
+ }
+ return _u
}
// SetCreatedAt sets the "created_at" field.
-func (ptuo *PasswordTokenUpdateOne) SetCreatedAt(t time.Time) *PasswordTokenUpdateOne {
- ptuo.mutation.SetCreatedAt(t)
- return ptuo
+func (_u *PasswordTokenUpdateOne) SetCreatedAt(v time.Time) *PasswordTokenUpdateOne {
+ _u.mutation.SetCreatedAt(v)
+ return _u
}
// SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
-func (ptuo *PasswordTokenUpdateOne) SetNillableCreatedAt(t *time.Time) *PasswordTokenUpdateOne {
- if t != nil {
- ptuo.SetCreatedAt(*t)
+func (_u *PasswordTokenUpdateOne) SetNillableCreatedAt(v *time.Time) *PasswordTokenUpdateOne {
+ if v != nil {
+ _u.SetCreatedAt(*v)
}
- return ptuo
-}
-
-// SetUserID sets the "user" edge to the User entity by ID.
-func (ptuo *PasswordTokenUpdateOne) SetUserID(id int) *PasswordTokenUpdateOne {
- ptuo.mutation.SetUserID(id)
- return ptuo
+ return _u
}
// SetUser sets the "user" edge to the User entity.
-func (ptuo *PasswordTokenUpdateOne) SetUser(u *User) *PasswordTokenUpdateOne {
- return ptuo.SetUserID(u.ID)
+func (_u *PasswordTokenUpdateOne) SetUser(v *User) *PasswordTokenUpdateOne {
+ return _u.SetUserID(v.ID)
}
// Mutation returns the PasswordTokenMutation object of the builder.
-func (ptuo *PasswordTokenUpdateOne) Mutation() *PasswordTokenMutation {
- return ptuo.mutation
+func (_u *PasswordTokenUpdateOne) Mutation() *PasswordTokenMutation {
+ return _u.mutation
}
// ClearUser clears the "user" edge to the User entity.
-func (ptuo *PasswordTokenUpdateOne) ClearUser() *PasswordTokenUpdateOne {
- ptuo.mutation.ClearUser()
- return ptuo
+func (_u *PasswordTokenUpdateOne) ClearUser() *PasswordTokenUpdateOne {
+ _u.mutation.ClearUser()
+ return _u
+}
+
+// Where appends a list predicates to the PasswordTokenUpdate builder.
+func (_u *PasswordTokenUpdateOne) Where(ps ...predicate.PasswordToken) *PasswordTokenUpdateOne {
+ _u.mutation.Where(ps...)
+ return _u
}
// Select allows selecting one or more fields (columns) of the returned entity.
// The default is selecting all fields defined in the entity schema.
-func (ptuo *PasswordTokenUpdateOne) Select(field string, fields ...string) *PasswordTokenUpdateOne {
- ptuo.fields = append([]string{field}, fields...)
- return ptuo
+func (_u *PasswordTokenUpdateOne) Select(field string, fields ...string) *PasswordTokenUpdateOne {
+ _u.fields = append([]string{field}, fields...)
+ return _u
}
// Save executes the query and returns the updated PasswordToken entity.
-func (ptuo *PasswordTokenUpdateOne) Save(ctx context.Context) (*PasswordToken, error) {
- var (
- err error
- node *PasswordToken
- )
- if len(ptuo.hooks) == 0 {
- if err = ptuo.check(); err != nil {
- return nil, err
- }
- node, err = ptuo.sqlSave(ctx)
- } else {
- var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
- mutation, ok := m.(*PasswordTokenMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T", m)
- }
- if err = ptuo.check(); err != nil {
- return nil, err
- }
- ptuo.mutation = mutation
- node, err = ptuo.sqlSave(ctx)
- mutation.done = true
- return node, err
- })
- for i := len(ptuo.hooks) - 1; i >= 0; i-- {
- if ptuo.hooks[i] == nil {
- return nil, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)")
- }
- mut = ptuo.hooks[i](mut)
- }
- if _, err := mut.Mutate(ctx, ptuo.mutation); err != nil {
- return nil, err
- }
- }
- return node, err
+func (_u *PasswordTokenUpdateOne) Save(ctx context.Context) (*PasswordToken, error) {
+ return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
}
// SaveX is like Save, but panics if an error occurs.
-func (ptuo *PasswordTokenUpdateOne) SaveX(ctx context.Context) *PasswordToken {
- node, err := ptuo.Save(ctx)
+func (_u *PasswordTokenUpdateOne) SaveX(ctx context.Context) *PasswordToken {
+ node, err := _u.Save(ctx)
if err != nil {
panic(err)
}
@@ -327,48 +280,42 @@ func (ptuo *PasswordTokenUpdateOne) SaveX(ctx context.Context) *PasswordToken {
}
// Exec executes the query on the entity.
-func (ptuo *PasswordTokenUpdateOne) Exec(ctx context.Context) error {
- _, err := ptuo.Save(ctx)
+func (_u *PasswordTokenUpdateOne) Exec(ctx context.Context) error {
+ _, err := _u.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
-func (ptuo *PasswordTokenUpdateOne) ExecX(ctx context.Context) {
- if err := ptuo.Exec(ctx); err != nil {
+func (_u *PasswordTokenUpdateOne) ExecX(ctx context.Context) {
+ if err := _u.Exec(ctx); err != nil {
panic(err)
}
}
// check runs all checks and user-defined validators on the builder.
-func (ptuo *PasswordTokenUpdateOne) check() error {
- if v, ok := ptuo.mutation.Hash(); ok {
- if err := passwordtoken.HashValidator(v); err != nil {
- return &ValidationError{Name: "hash", err: fmt.Errorf("ent: validator failed for field \"hash\": %w", err)}
+func (_u *PasswordTokenUpdateOne) check() error {
+ if v, ok := _u.mutation.Token(); ok {
+ if err := passwordtoken.TokenValidator(v); err != nil {
+ return &ValidationError{Name: "token", err: fmt.Errorf(`ent: validator failed for field "PasswordToken.token": %w`, err)}
}
}
- if _, ok := ptuo.mutation.UserID(); ptuo.mutation.UserCleared() && !ok {
- return errors.New("ent: clearing a required unique edge \"user\"")
+ if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
+ return errors.New(`ent: clearing a required unique edge "PasswordToken.user"`)
}
return nil
}
-func (ptuo *PasswordTokenUpdateOne) sqlSave(ctx context.Context) (_node *PasswordToken, err error) {
- _spec := &sqlgraph.UpdateSpec{
- Node: &sqlgraph.NodeSpec{
- Table: passwordtoken.Table,
- Columns: passwordtoken.Columns,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
- },
+func (_u *PasswordTokenUpdateOne) sqlSave(ctx context.Context) (_node *PasswordToken, err error) {
+ if err := _u.check(); err != nil {
+ return _node, err
}
- id, ok := ptuo.mutation.ID()
+ _spec := sqlgraph.NewUpdateSpec(passwordtoken.Table, passwordtoken.Columns, sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt))
+ id, ok := _u.mutation.ID()
if !ok {
- return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing PasswordToken.ID for update")}
+ return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "PasswordToken.id" for update`)}
}
_spec.Node.ID.Value = id
- if fields := ptuo.fields; len(fields) > 0 {
+ if fields := _u.fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, passwordtoken.FieldID)
for _, f := range fields {
@@ -380,28 +327,20 @@ func (ptuo *PasswordTokenUpdateOne) sqlSave(ctx context.Context) (_node *Passwor
}
}
}
- if ps := ptuo.mutation.predicates; len(ps) > 0 {
+ if ps := _u.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
- if value, ok := ptuo.mutation.Hash(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: passwordtoken.FieldHash,
- })
+ if value, ok := _u.mutation.Token(); ok {
+ _spec.SetField(passwordtoken.FieldToken, field.TypeString, value)
}
- if value, ok := ptuo.mutation.CreatedAt(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeTime,
- Value: value,
- Column: passwordtoken.FieldCreatedAt,
- })
+ if value, ok := _u.mutation.CreatedAt(); ok {
+ _spec.SetField(passwordtoken.FieldCreatedAt, field.TypeTime, value)
}
- if ptuo.mutation.UserCleared() {
+ if _u.mutation.UserCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
@@ -409,15 +348,12 @@ func (ptuo *PasswordTokenUpdateOne) sqlSave(ctx context.Context) (_node *Passwor
Columns: []string{passwordtoken.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
- if nodes := ptuo.mutation.UserIDs(); len(nodes) > 0 {
+ if nodes := _u.mutation.UserIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: false,
@@ -425,10 +361,7 @@ func (ptuo *PasswordTokenUpdateOne) sqlSave(ctx context.Context) (_node *Passwor
Columns: []string{passwordtoken.UserColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt),
},
}
for _, k := range nodes {
@@ -436,16 +369,17 @@ func (ptuo *PasswordTokenUpdateOne) sqlSave(ctx context.Context) (_node *Passwor
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
- _node = &PasswordToken{config: ptuo.config}
+ _node = &PasswordToken{config: _u.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues
- if err = sqlgraph.UpdateNode(ctx, ptuo.driver, _spec); err != nil {
+ if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{passwordtoken.Label}
} else if sqlgraph.IsConstraintError(err) {
- err = &ConstraintError{err.Error(), err}
+ err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
+ _u.mutation.done = true
return _node, nil
}
diff --git a/ent/predicate/predicate.go b/ent/predicate/predicate.go
index c10c042..ecc90c4 100644
--- a/ent/predicate/predicate.go
+++ b/ent/predicate/predicate.go
@@ -1,4 +1,4 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package predicate
diff --git a/ent/runtime.go b/ent/runtime.go
index e4bc5f5..33c9064 100644
--- a/ent/runtime.go
+++ b/ent/runtime.go
@@ -1,5 +1,5 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
-// The schema-stitching logic is generated in goweb/ent/runtime/runtime.go
+// The schema-stitching logic is generated in github.com/mikestefanello/pagoda/ent/runtime/runtime.go
diff --git a/ent/runtime/runtime.go b/ent/runtime/runtime.go
index 9c3be62..e76534e 100644
--- a/ent/runtime/runtime.go
+++ b/ent/runtime/runtime.go
@@ -1,26 +1,29 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package runtime
import (
- "goweb/ent/passwordtoken"
- "goweb/ent/schema"
- "goweb/ent/user"
"time"
+
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/schema"
+ "github.com/mikestefanello/pagoda/ent/user"
)
// The init function reads all schema descriptors with runtime code
// (default values, validators, hooks and policies) and stitches it
// to their package variables.
func init() {
+ passwordtokenHooks := schema.PasswordToken{}.Hooks()
+ passwordtoken.Hooks[0] = passwordtokenHooks[0]
passwordtokenFields := schema.PasswordToken{}.Fields()
_ = passwordtokenFields
- // passwordtokenDescHash is the schema descriptor for hash field.
- passwordtokenDescHash := passwordtokenFields[0].Descriptor()
- // passwordtoken.HashValidator is a validator for the "hash" field. It is called by the builders before save.
- passwordtoken.HashValidator = passwordtokenDescHash.Validators[0].(func(string) error)
+ // passwordtokenDescToken is the schema descriptor for token field.
+ passwordtokenDescToken := passwordtokenFields[0].Descriptor()
+ // passwordtoken.TokenValidator is a validator for the "token" field. It is called by the builders before save.
+ passwordtoken.TokenValidator = passwordtokenDescToken.Validators[0].(func(string) error)
// passwordtokenDescCreatedAt is the schema descriptor for created_at field.
- passwordtokenDescCreatedAt := passwordtokenFields[1].Descriptor()
+ passwordtokenDescCreatedAt := passwordtokenFields[2].Descriptor()
// passwordtoken.DefaultCreatedAt holds the default value on creation for the created_at field.
passwordtoken.DefaultCreatedAt = passwordtokenDescCreatedAt.Default.(func() time.Time)
userHooks := schema.User{}.Hooks()
@@ -34,18 +37,40 @@ func init() {
// userDescEmail is the schema descriptor for email field.
userDescEmail := userFields[1].Descriptor()
// user.EmailValidator is a validator for the "email" field. It is called by the builders before save.
- user.EmailValidator = userDescEmail.Validators[0].(func(string) error)
+ user.EmailValidator = func() func(string) error {
+ validators := userDescEmail.Validators
+ fns := [...]func(string) error{
+ validators[0].(func(string) error),
+ validators[1].(func(string) error),
+ }
+ return func(email string) error {
+ for _, fn := range fns {
+ if err := fn(email); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+ }()
// userDescPassword is the schema descriptor for password field.
userDescPassword := userFields[2].Descriptor()
// user.PasswordValidator is a validator for the "password" field. It is called by the builders before save.
user.PasswordValidator = userDescPassword.Validators[0].(func(string) error)
+ // userDescVerified is the schema descriptor for verified field.
+ userDescVerified := userFields[3].Descriptor()
+ // user.DefaultVerified holds the default value on creation for the verified field.
+ user.DefaultVerified = userDescVerified.Default.(bool)
+ // userDescAdmin is the schema descriptor for admin field.
+ userDescAdmin := userFields[4].Descriptor()
+ // user.DefaultAdmin holds the default value on creation for the admin field.
+ user.DefaultAdmin = userDescAdmin.Default.(bool)
// userDescCreatedAt is the schema descriptor for created_at field.
- userDescCreatedAt := userFields[3].Descriptor()
+ userDescCreatedAt := userFields[5].Descriptor()
// user.DefaultCreatedAt holds the default value on creation for the created_at field.
user.DefaultCreatedAt = userDescCreatedAt.Default.(func() time.Time)
}
const (
- Version = "v0.9.1" // Version of ent codegen.
- Sum = "h1:IG8andyeD79GG24U8Q+1Y45hQXj6gY5evSBcva5gtBk=" // Sum of ent codegen.
+ Version = "v0.14.5" // Version of ent codegen.
+ Sum = "h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=" // Sum of ent codegen.
)
diff --git a/ent/schema/passwordtoken.go b/ent/schema/passwordtoken.go
index e518d16..12bdf20 100644
--- a/ent/schema/passwordtoken.go
+++ b/ent/schema/passwordtoken.go
@@ -1,11 +1,15 @@
package schema
import (
+ "context"
"time"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
+ ge "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/ent/hook"
+ "golang.org/x/crypto/bcrypt"
)
// PasswordToken holds the schema definition for the PasswordToken entity.
@@ -16,9 +20,10 @@ type PasswordToken struct {
// Fields of the PasswordToken.
func (PasswordToken) Fields() []ent.Field {
return []ent.Field{
- field.String("hash").
+ field.String("token").
Sensitive().
NotEmpty(),
+ field.Int("user_id"),
field.Time("created_at").
Default(time.Now),
}
@@ -28,7 +33,30 @@ func (PasswordToken) Fields() []ent.Field {
func (PasswordToken) Edges() []ent.Edge {
return []ent.Edge{
edge.To("user", User.Type).
+ Field("user_id").
Required().
Unique(),
}
}
+
+// Hooks of the PasswordToken.
+func (PasswordToken) Hooks() []ent.Hook {
+ return []ent.Hook{
+ hook.On(
+ func(next ent.Mutator) ent.Mutator {
+ return hook.PasswordTokenFunc(func(ctx context.Context, m *ge.PasswordTokenMutation) (ent.Value, error) {
+ if v, exists := m.Token(); exists {
+ hash, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost)
+ if err != nil {
+ return "", err
+ }
+ m.SetToken(string(hash))
+ }
+ return next.Mutate(ctx, m)
+ })
+ },
+ // Limit the hook only for these operations.
+ ent.OpCreate|ent.OpUpdate|ent.OpUpdateOne,
+ ),
+ }
+}
diff --git a/ent/schema/user.go b/ent/schema/user.go
index 9d2a79b..ac2d35e 100644
--- a/ent/schema/user.go
+++ b/ent/schema/user.go
@@ -2,11 +2,13 @@ package schema
import (
"context"
+ "net/mail"
"strings"
"time"
- ge "goweb/ent"
- "goweb/ent/hook"
+ ge "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/ent/hook"
+ "golang.org/x/crypto/bcrypt"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
@@ -25,10 +27,18 @@ func (User) Fields() []ent.Field {
NotEmpty(),
field.String("email").
NotEmpty().
- Unique(),
+ Unique().
+ Validate(func(s string) error {
+ _, err := mail.ParseAddress(s)
+ return err
+ }),
field.String("password").
Sensitive().
NotEmpty(),
+ field.Bool("verified").
+ Default(false),
+ field.Bool("admin").
+ Default(false),
field.Time("created_at").
Default(time.Now).
Immutable(),
@@ -52,6 +62,14 @@ func (User) Hooks() []ent.Hook {
if v, exists := m.Email(); exists {
m.SetEmail(strings.ToLower(v))
}
+
+ if v, exists := m.Password(); exists {
+ hash, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost)
+ if err != nil {
+ return "", err
+ }
+ m.SetPassword(string(hash))
+ }
return next.Mutate(ctx, m)
})
},
diff --git a/ent/tx.go b/ent/tx.go
index 7b4c06a..6d1cb6c 100644
--- a/ent/tx.go
+++ b/ent/tx.go
@@ -1,4 +1,4 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
@@ -20,19 +20,13 @@ type Tx struct {
// lazily loaded.
client *Client
clientOnce sync.Once
-
- // completion callbacks.
- mu sync.Mutex
- onCommit []CommitHook
- onRollback []RollbackHook
-
// ctx lives for the life of the transaction. It is
// the same context used by the underlying connection.
ctx context.Context
}
type (
- // Committer is the interface that wraps the Committer method.
+ // Committer is the interface that wraps the Commit method.
Committer interface {
Commit(context.Context, *Tx) error
}
@@ -46,7 +40,7 @@ type (
// and returns a Committer. For example:
//
// hook := func(next ent.Committer) ent.Committer {
- // return ent.CommitFunc(func(context.Context, tx *ent.Tx) error {
+ // return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
// // Do some stuff before.
// if err := next.Commit(ctx, tx); err != nil {
// return err
@@ -70,9 +64,9 @@ func (tx *Tx) Commit() error {
var fn Committer = CommitFunc(func(context.Context, *Tx) error {
return txDriver.tx.Commit()
})
- tx.mu.Lock()
- hooks := append([]CommitHook(nil), tx.onCommit...)
- tx.mu.Unlock()
+ txDriver.mu.Lock()
+ hooks := append([]CommitHook(nil), txDriver.onCommit...)
+ txDriver.mu.Unlock()
for i := len(hooks) - 1; i >= 0; i-- {
fn = hooks[i](fn)
}
@@ -81,13 +75,14 @@ func (tx *Tx) Commit() error {
// OnCommit adds a hook to call on commit.
func (tx *Tx) OnCommit(f CommitHook) {
- tx.mu.Lock()
- defer tx.mu.Unlock()
- tx.onCommit = append(tx.onCommit, f)
+ txDriver := tx.config.driver.(*txDriver)
+ txDriver.mu.Lock()
+ txDriver.onCommit = append(txDriver.onCommit, f)
+ txDriver.mu.Unlock()
}
type (
- // Rollbacker is the interface that wraps the Rollbacker method.
+ // Rollbacker is the interface that wraps the Rollback method.
Rollbacker interface {
Rollback(context.Context, *Tx) error
}
@@ -101,7 +96,7 @@ type (
// and returns a Rollbacker. For example:
//
// hook := func(next ent.Rollbacker) ent.Rollbacker {
- // return ent.RollbackFunc(func(context.Context, tx *ent.Tx) error {
+ // return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error {
// // Do some stuff before.
// if err := next.Rollback(ctx, tx); err != nil {
// return err
@@ -125,9 +120,9 @@ func (tx *Tx) Rollback() error {
var fn Rollbacker = RollbackFunc(func(context.Context, *Tx) error {
return txDriver.tx.Rollback()
})
- tx.mu.Lock()
- hooks := append([]RollbackHook(nil), tx.onRollback...)
- tx.mu.Unlock()
+ txDriver.mu.Lock()
+ hooks := append([]RollbackHook(nil), txDriver.onRollback...)
+ txDriver.mu.Unlock()
for i := len(hooks) - 1; i >= 0; i-- {
fn = hooks[i](fn)
}
@@ -136,9 +131,10 @@ func (tx *Tx) Rollback() error {
// OnRollback adds a hook to call on rollback.
func (tx *Tx) OnRollback(f RollbackHook) {
- tx.mu.Lock()
- defer tx.mu.Unlock()
- tx.onRollback = append(tx.onRollback, f)
+ txDriver := tx.config.driver.(*txDriver)
+ txDriver.mu.Lock()
+ txDriver.onRollback = append(txDriver.onRollback, f)
+ txDriver.mu.Unlock()
}
// Client returns a Client that binds to current transaction.
@@ -171,6 +167,10 @@ type txDriver struct {
drv dialect.Driver
// tx is the underlying transaction.
tx dialect.Tx
+ // completion hooks.
+ mu sync.Mutex
+ onCommit []CommitHook
+ onRollback []RollbackHook
}
// newTx creates a new transactional driver.
@@ -201,12 +201,12 @@ func (*txDriver) Commit() error { return nil }
func (*txDriver) Rollback() error { return nil }
// Exec calls tx.Exec.
-func (tx *txDriver) Exec(ctx context.Context, query string, args, v interface{}) error {
+func (tx *txDriver) Exec(ctx context.Context, query string, args, v any) error {
return tx.tx.Exec(ctx, query, args, v)
}
// Query calls tx.Query.
-func (tx *txDriver) Query(ctx context.Context, query string, args, v interface{}) error {
+func (tx *txDriver) Query(ctx context.Context, query string, args, v any) error {
return tx.tx.Query(ctx, query, args, v)
}
diff --git a/ent/user.go b/ent/user.go
index 2dfe64a..11a2015 100644
--- a/ent/user.go
+++ b/ent/user.go
@@ -1,14 +1,15 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
import (
"fmt"
- "goweb/ent/user"
"strings"
"time"
+ "entgo.io/ent"
"entgo.io/ent/dialect/sql"
+ "github.com/mikestefanello/pagoda/ent/user"
)
// User is the model entity for the User schema.
@@ -22,11 +23,16 @@ type User struct {
Email string `json:"email,omitempty"`
// Password holds the value of the "password" field.
Password string `json:"-"`
+ // Verified holds the value of the "verified" field.
+ Verified bool `json:"verified,omitempty"`
+ // Admin holds the value of the "admin" field.
+ Admin bool `json:"admin,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 UserQuery when eager-loading is set.
- Edges UserEdges `json:"edges"`
+ Edges UserEdges `json:"edges"`
+ selectValues sql.SelectValues
}
// UserEdges holds the relations/edges for other nodes in the graph.
@@ -48,10 +54,12 @@ func (e UserEdges) OwnerOrErr() ([]*PasswordToken, error) {
}
// scanValues returns the types for scanning values from sql.Rows.
-func (*User) scanValues(columns []string) ([]interface{}, error) {
- values := make([]interface{}, len(columns))
+func (*User) scanValues(columns []string) ([]any, error) {
+ values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
+ case user.FieldVerified, user.FieldAdmin:
+ values[i] = new(sql.NullBool)
case user.FieldID:
values[i] = new(sql.NullInt64)
case user.FieldName, user.FieldEmail, user.FieldPassword:
@@ -59,7 +67,7 @@ func (*User) scanValues(columns []string) ([]interface{}, error) {
case user.FieldCreatedAt:
values[i] = new(sql.NullTime)
default:
- return nil, fmt.Errorf("unexpected column %q for type User", columns[i])
+ values[i] = new(sql.UnknownType)
}
}
return values, nil
@@ -67,7 +75,7 @@ func (*User) scanValues(columns []string) ([]interface{}, error) {
// assignValues assigns the values that were returned from sql.Rows (after scanning)
// to the User fields.
-func (u *User) assignValues(columns []string, values []interface{}) error {
+func (_m *User) 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)
}
@@ -78,80 +86,103 @@ func (u *User) assignValues(columns []string, values []interface{}) error {
if !ok {
return fmt.Errorf("unexpected type %T for field id", value)
}
- u.ID = int(value.Int64)
+ _m.ID = int(value.Int64)
case user.FieldName:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field name", values[i])
} else if value.Valid {
- u.Name = value.String
+ _m.Name = value.String
}
case user.FieldEmail:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field email", values[i])
} else if value.Valid {
- u.Email = value.String
+ _m.Email = value.String
}
case user.FieldPassword:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field password", values[i])
} else if value.Valid {
- u.Password = value.String
+ _m.Password = value.String
+ }
+ case user.FieldVerified:
+ if value, ok := values[i].(*sql.NullBool); !ok {
+ return fmt.Errorf("unexpected type %T for field verified", values[i])
+ } else if value.Valid {
+ _m.Verified = value.Bool
+ }
+ case user.FieldAdmin:
+ if value, ok := values[i].(*sql.NullBool); !ok {
+ return fmt.Errorf("unexpected type %T for field admin", values[i])
+ } else if value.Valid {
+ _m.Admin = value.Bool
}
case user.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 {
- u.CreatedAt = value.Time
+ _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 User.
+// This includes values selected through modifiers, order, etc.
+func (_m *User) Value(name string) (ent.Value, error) {
+ return _m.selectValues.Get(name)
+}
+
// QueryOwner queries the "owner" edge of the User entity.
-func (u *User) QueryOwner() *PasswordTokenQuery {
- return (&UserClient{config: u.config}).QueryOwner(u)
+func (_m *User) QueryOwner() *PasswordTokenQuery {
+ return NewUserClient(_m.config).QueryOwner(_m)
}
// Update returns a builder for updating this User.
// Note that you need to call User.Unwrap() before calling this method if this User
// was returned from a transaction, and the transaction was committed or rolled back.
-func (u *User) Update() *UserUpdateOne {
- return (&UserClient{config: u.config}).UpdateOne(u)
+func (_m *User) Update() *UserUpdateOne {
+ return NewUserClient(_m.config).UpdateOne(_m)
}
// Unwrap unwraps the User 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 (u *User) Unwrap() *User {
- tx, ok := u.config.driver.(*txDriver)
+func (_m *User) Unwrap() *User {
+ _tx, ok := _m.config.driver.(*txDriver)
if !ok {
panic("ent: User is not a transactional entity")
}
- u.config.driver = tx.drv
- return u
+ _m.config.driver = _tx.drv
+ return _m
}
// String implements the fmt.Stringer.
-func (u *User) String() string {
+func (_m *User) String() string {
var builder strings.Builder
builder.WriteString("User(")
- builder.WriteString(fmt.Sprintf("id=%v", u.ID))
- builder.WriteString(", name=")
- builder.WriteString(u.Name)
- builder.WriteString(", email=")
- builder.WriteString(u.Email)
- builder.WriteString(", password=")
- builder.WriteString(", created_at=")
- builder.WriteString(u.CreatedAt.Format(time.ANSIC))
+ builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
+ builder.WriteString("name=")
+ builder.WriteString(_m.Name)
+ builder.WriteString(", ")
+ builder.WriteString("email=")
+ builder.WriteString(_m.Email)
+ builder.WriteString(", ")
+ builder.WriteString("password=")
+ builder.WriteString(", ")
+ builder.WriteString("verified=")
+ builder.WriteString(fmt.Sprintf("%v", _m.Verified))
+ builder.WriteString(", ")
+ builder.WriteString("admin=")
+ builder.WriteString(fmt.Sprintf("%v", _m.Admin))
+ builder.WriteString(", ")
+ builder.WriteString("created_at=")
+ builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
builder.WriteByte(')')
return builder.String()
}
// Users is a parsable slice of User.
type Users []*User
-
-func (u Users) config(cfg config) {
- for _i := range u {
- u[_i].config = cfg
- }
-}
diff --git a/ent/user/user.go b/ent/user/user.go
index 4d7a487..d0c7dc7 100644
--- a/ent/user/user.go
+++ b/ent/user/user.go
@@ -1,4 +1,4 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package user
@@ -6,6 +6,8 @@ import (
"time"
"entgo.io/ent"
+ "entgo.io/ent/dialect/sql"
+ "entgo.io/ent/dialect/sql/sqlgraph"
)
const (
@@ -19,6 +21,10 @@ const (
FieldEmail = "email"
// FieldPassword holds the string denoting the password field in the database.
FieldPassword = "password"
+ // FieldVerified holds the string denoting the verified field in the database.
+ FieldVerified = "verified"
+ // FieldAdmin holds the string denoting the admin field in the database.
+ FieldAdmin = "admin"
// FieldCreatedAt holds the string denoting the created_at field in the database.
FieldCreatedAt = "created_at"
// EdgeOwner holds the string denoting the owner edge name in mutations.
@@ -31,7 +37,7 @@ const (
// It exists in this package in order to avoid circular dependency with the "passwordtoken" package.
OwnerInverseTable = "password_tokens"
// OwnerColumn is the table column denoting the owner relation/edge.
- OwnerColumn = "password_token_user"
+ OwnerColumn = "user_id"
)
// Columns holds all SQL columns for user fields.
@@ -40,6 +46,8 @@ var Columns = []string{
FieldName,
FieldEmail,
FieldPassword,
+ FieldVerified,
+ FieldAdmin,
FieldCreatedAt,
}
@@ -57,8 +65,7 @@ func ValidColumn(column string) bool {
// package on the initialization of the application. Therefore,
// it should be imported in the main as follows:
//
-// import _ "goweb/ent/runtime"
-//
+// import _ "github.com/mikestefanello/pagoda/ent/runtime"
var (
Hooks [1]ent.Hook
// NameValidator is a validator for the "name" field. It is called by the builders before save.
@@ -67,6 +74,69 @@ var (
EmailValidator func(string) error
// PasswordValidator is a validator for the "password" field. It is called by the builders before save.
PasswordValidator func(string) error
+ // DefaultVerified holds the default value on creation for the "verified" field.
+ DefaultVerified bool
+ // DefaultAdmin holds the default value on creation for the "admin" field.
+ DefaultAdmin bool
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
DefaultCreatedAt func() time.Time
)
+
+// OrderOption defines the ordering options for the User queries.
+type OrderOption func(*sql.Selector)
+
+// ByID orders the results by the id field.
+func ByID(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldID, opts...).ToFunc()
+}
+
+// ByName orders the results by the name field.
+func ByName(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldName, opts...).ToFunc()
+}
+
+// ByEmail orders the results by the email field.
+func ByEmail(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldEmail, opts...).ToFunc()
+}
+
+// ByPassword orders the results by the password field.
+func ByPassword(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldPassword, opts...).ToFunc()
+}
+
+// ByVerified orders the results by the verified field.
+func ByVerified(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldVerified, opts...).ToFunc()
+}
+
+// ByAdmin orders the results by the admin field.
+func ByAdmin(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldAdmin, opts...).ToFunc()
+}
+
+// ByCreatedAt orders the results by the created_at field.
+func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
+}
+
+// ByOwnerCount orders the results by owner count.
+func ByOwnerCount(opts ...sql.OrderTermOption) OrderOption {
+ return func(s *sql.Selector) {
+ sqlgraph.OrderByNeighborsCount(s, newOwnerStep(), opts...)
+ }
+}
+
+// ByOwner orders the results by owner terms.
+func ByOwner(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
+ return func(s *sql.Selector) {
+ sqlgraph.OrderByNeighborTerms(s, newOwnerStep(), append([]sql.OrderTerm{term}, terms...)...)
+ }
+}
+func newOwnerStep() *sqlgraph.Step {
+ return sqlgraph.NewStep(
+ sqlgraph.From(Table, FieldID),
+ sqlgraph.To(OwnerInverseTable, FieldID),
+ sqlgraph.Edge(sqlgraph.O2M, true, OwnerTable, OwnerColumn),
+ )
+}
diff --git a/ent/user/where.go b/ent/user/where.go
index 550ba77..064afdb 100644
--- a/ent/user/where.go
+++ b/ent/user/where.go
@@ -1,533 +1,343 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package user
import (
- "goweb/ent/predicate"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
+ "github.com/mikestefanello/pagoda/ent/predicate"
)
// ID filters vertices based on their ID field.
func ID(id int) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldID), id))
- })
+ return predicate.User(sql.FieldEQ(FieldID, id))
}
// IDEQ applies the EQ predicate on the ID field.
func IDEQ(id int) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldID), id))
- })
+ return predicate.User(sql.FieldEQ(FieldID, id))
}
// IDNEQ applies the NEQ predicate on the ID field.
func IDNEQ(id int) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.NEQ(s.C(FieldID), id))
- })
+ return predicate.User(sql.FieldNEQ(FieldID, id))
}
// IDIn applies the In predicate on the ID field.
func IDIn(ids ...int) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(ids) == 0 {
- s.Where(sql.False())
- return
- }
- v := make([]interface{}, len(ids))
- for i := range v {
- v[i] = ids[i]
- }
- s.Where(sql.In(s.C(FieldID), v...))
- })
+ return predicate.User(sql.FieldIn(FieldID, ids...))
}
// IDNotIn applies the NotIn predicate on the ID field.
func IDNotIn(ids ...int) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(ids) == 0 {
- s.Where(sql.False())
- return
- }
- v := make([]interface{}, len(ids))
- for i := range v {
- v[i] = ids[i]
- }
- s.Where(sql.NotIn(s.C(FieldID), v...))
- })
+ return predicate.User(sql.FieldNotIn(FieldID, ids...))
}
// IDGT applies the GT predicate on the ID field.
func IDGT(id int) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GT(s.C(FieldID), id))
- })
+ return predicate.User(sql.FieldGT(FieldID, id))
}
// IDGTE applies the GTE predicate on the ID field.
func IDGTE(id int) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GTE(s.C(FieldID), id))
- })
+ return predicate.User(sql.FieldGTE(FieldID, id))
}
// IDLT applies the LT predicate on the ID field.
func IDLT(id int) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LT(s.C(FieldID), id))
- })
+ return predicate.User(sql.FieldLT(FieldID, id))
}
// IDLTE applies the LTE predicate on the ID field.
func IDLTE(id int) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LTE(s.C(FieldID), id))
- })
+ return predicate.User(sql.FieldLTE(FieldID, id))
}
// Name applies equality check predicate on the "name" field. It's identical to NameEQ.
func Name(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldEQ(FieldName, v))
}
// Email applies equality check predicate on the "email" field. It's identical to EmailEQ.
func Email(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldEQ(FieldEmail, v))
}
// Password applies equality check predicate on the "password" field. It's identical to PasswordEQ.
func Password(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldEQ(FieldPassword, v))
+}
+
+// Verified applies equality check predicate on the "verified" field. It's identical to VerifiedEQ.
+func Verified(v bool) predicate.User {
+ return predicate.User(sql.FieldEQ(FieldVerified, v))
+}
+
+// Admin applies equality check predicate on the "admin" field. It's identical to AdminEQ.
+func Admin(v bool) predicate.User {
+ return predicate.User(sql.FieldEQ(FieldAdmin, v))
}
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
func CreatedAt(v time.Time) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldCreatedAt), v))
- })
+ return predicate.User(sql.FieldEQ(FieldCreatedAt, v))
}
// NameEQ applies the EQ predicate on the "name" field.
func NameEQ(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldEQ(FieldName, v))
}
// NameNEQ applies the NEQ predicate on the "name" field.
func NameNEQ(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.NEQ(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldNEQ(FieldName, v))
}
// NameIn applies the In predicate on the "name" field.
func NameIn(vs ...string) predicate.User {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.In(s.C(FieldName), v...))
- })
+ return predicate.User(sql.FieldIn(FieldName, vs...))
}
// NameNotIn applies the NotIn predicate on the "name" field.
func NameNotIn(vs ...string) predicate.User {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.NotIn(s.C(FieldName), v...))
- })
+ return predicate.User(sql.FieldNotIn(FieldName, vs...))
}
// NameGT applies the GT predicate on the "name" field.
func NameGT(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GT(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldGT(FieldName, v))
}
// NameGTE applies the GTE predicate on the "name" field.
func NameGTE(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GTE(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldGTE(FieldName, v))
}
// NameLT applies the LT predicate on the "name" field.
func NameLT(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LT(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldLT(FieldName, v))
}
// NameLTE applies the LTE predicate on the "name" field.
func NameLTE(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LTE(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldLTE(FieldName, v))
}
// NameContains applies the Contains predicate on the "name" field.
func NameContains(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.Contains(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldContains(FieldName, v))
}
// NameHasPrefix applies the HasPrefix predicate on the "name" field.
func NameHasPrefix(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.HasPrefix(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldHasPrefix(FieldName, v))
}
// NameHasSuffix applies the HasSuffix predicate on the "name" field.
func NameHasSuffix(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.HasSuffix(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldHasSuffix(FieldName, v))
}
// NameEqualFold applies the EqualFold predicate on the "name" field.
func NameEqualFold(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EqualFold(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldEqualFold(FieldName, v))
}
// NameContainsFold applies the ContainsFold predicate on the "name" field.
func NameContainsFold(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.ContainsFold(s.C(FieldName), v))
- })
+ return predicate.User(sql.FieldContainsFold(FieldName, v))
}
// EmailEQ applies the EQ predicate on the "email" field.
func EmailEQ(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldEQ(FieldEmail, v))
}
// EmailNEQ applies the NEQ predicate on the "email" field.
func EmailNEQ(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.NEQ(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldNEQ(FieldEmail, v))
}
// EmailIn applies the In predicate on the "email" field.
func EmailIn(vs ...string) predicate.User {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.In(s.C(FieldEmail), v...))
- })
+ return predicate.User(sql.FieldIn(FieldEmail, vs...))
}
// EmailNotIn applies the NotIn predicate on the "email" field.
func EmailNotIn(vs ...string) predicate.User {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.NotIn(s.C(FieldEmail), v...))
- })
+ return predicate.User(sql.FieldNotIn(FieldEmail, vs...))
}
// EmailGT applies the GT predicate on the "email" field.
func EmailGT(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GT(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldGT(FieldEmail, v))
}
// EmailGTE applies the GTE predicate on the "email" field.
func EmailGTE(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GTE(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldGTE(FieldEmail, v))
}
// EmailLT applies the LT predicate on the "email" field.
func EmailLT(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LT(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldLT(FieldEmail, v))
}
// EmailLTE applies the LTE predicate on the "email" field.
func EmailLTE(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LTE(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldLTE(FieldEmail, v))
}
// EmailContains applies the Contains predicate on the "email" field.
func EmailContains(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.Contains(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldContains(FieldEmail, v))
}
// EmailHasPrefix applies the HasPrefix predicate on the "email" field.
func EmailHasPrefix(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.HasPrefix(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldHasPrefix(FieldEmail, v))
}
// EmailHasSuffix applies the HasSuffix predicate on the "email" field.
func EmailHasSuffix(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.HasSuffix(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldHasSuffix(FieldEmail, v))
}
// EmailEqualFold applies the EqualFold predicate on the "email" field.
func EmailEqualFold(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EqualFold(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldEqualFold(FieldEmail, v))
}
// EmailContainsFold applies the ContainsFold predicate on the "email" field.
func EmailContainsFold(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.ContainsFold(s.C(FieldEmail), v))
- })
+ return predicate.User(sql.FieldContainsFold(FieldEmail, v))
}
// PasswordEQ applies the EQ predicate on the "password" field.
func PasswordEQ(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldEQ(FieldPassword, v))
}
// PasswordNEQ applies the NEQ predicate on the "password" field.
func PasswordNEQ(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.NEQ(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldNEQ(FieldPassword, v))
}
// PasswordIn applies the In predicate on the "password" field.
func PasswordIn(vs ...string) predicate.User {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.In(s.C(FieldPassword), v...))
- })
+ return predicate.User(sql.FieldIn(FieldPassword, vs...))
}
// PasswordNotIn applies the NotIn predicate on the "password" field.
func PasswordNotIn(vs ...string) predicate.User {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.NotIn(s.C(FieldPassword), v...))
- })
+ return predicate.User(sql.FieldNotIn(FieldPassword, vs...))
}
// PasswordGT applies the GT predicate on the "password" field.
func PasswordGT(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GT(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldGT(FieldPassword, v))
}
// PasswordGTE applies the GTE predicate on the "password" field.
func PasswordGTE(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GTE(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldGTE(FieldPassword, v))
}
// PasswordLT applies the LT predicate on the "password" field.
func PasswordLT(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LT(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldLT(FieldPassword, v))
}
// PasswordLTE applies the LTE predicate on the "password" field.
func PasswordLTE(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LTE(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldLTE(FieldPassword, v))
}
// PasswordContains applies the Contains predicate on the "password" field.
func PasswordContains(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.Contains(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldContains(FieldPassword, v))
}
// PasswordHasPrefix applies the HasPrefix predicate on the "password" field.
func PasswordHasPrefix(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.HasPrefix(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldHasPrefix(FieldPassword, v))
}
// PasswordHasSuffix applies the HasSuffix predicate on the "password" field.
func PasswordHasSuffix(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.HasSuffix(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldHasSuffix(FieldPassword, v))
}
// PasswordEqualFold applies the EqualFold predicate on the "password" field.
func PasswordEqualFold(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EqualFold(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldEqualFold(FieldPassword, v))
}
// PasswordContainsFold applies the ContainsFold predicate on the "password" field.
func PasswordContainsFold(v string) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.ContainsFold(s.C(FieldPassword), v))
- })
+ return predicate.User(sql.FieldContainsFold(FieldPassword, v))
+}
+
+// VerifiedEQ applies the EQ predicate on the "verified" field.
+func VerifiedEQ(v bool) predicate.User {
+ return predicate.User(sql.FieldEQ(FieldVerified, v))
+}
+
+// VerifiedNEQ applies the NEQ predicate on the "verified" field.
+func VerifiedNEQ(v bool) predicate.User {
+ return predicate.User(sql.FieldNEQ(FieldVerified, v))
+}
+
+// AdminEQ applies the EQ predicate on the "admin" field.
+func AdminEQ(v bool) predicate.User {
+ return predicate.User(sql.FieldEQ(FieldAdmin, v))
+}
+
+// AdminNEQ applies the NEQ predicate on the "admin" field.
+func AdminNEQ(v bool) predicate.User {
+ return predicate.User(sql.FieldNEQ(FieldAdmin, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.EQ(s.C(FieldCreatedAt), v))
- })
+ return predicate.User(sql.FieldEQ(FieldCreatedAt, v))
}
// CreatedAtNEQ applies the NEQ predicate on the "created_at" field.
func CreatedAtNEQ(v time.Time) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.NEQ(s.C(FieldCreatedAt), v))
- })
+ return predicate.User(sql.FieldNEQ(FieldCreatedAt, v))
}
// CreatedAtIn applies the In predicate on the "created_at" field.
func CreatedAtIn(vs ...time.Time) predicate.User {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.In(s.C(FieldCreatedAt), v...))
- })
+ return predicate.User(sql.FieldIn(FieldCreatedAt, vs...))
}
// CreatedAtNotIn applies the NotIn predicate on the "created_at" field.
func CreatedAtNotIn(vs ...time.Time) predicate.User {
- v := make([]interface{}, len(vs))
- for i := range v {
- v[i] = vs[i]
- }
- return predicate.User(func(s *sql.Selector) {
- // if not arguments were provided, append the FALSE constants,
- // since we can't apply "IN ()". This will make this predicate falsy.
- if len(v) == 0 {
- s.Where(sql.False())
- return
- }
- s.Where(sql.NotIn(s.C(FieldCreatedAt), v...))
- })
+ return predicate.User(sql.FieldNotIn(FieldCreatedAt, vs...))
}
// CreatedAtGT applies the GT predicate on the "created_at" field.
func CreatedAtGT(v time.Time) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GT(s.C(FieldCreatedAt), v))
- })
+ return predicate.User(sql.FieldGT(FieldCreatedAt, v))
}
// CreatedAtGTE applies the GTE predicate on the "created_at" field.
func CreatedAtGTE(v time.Time) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.GTE(s.C(FieldCreatedAt), v))
- })
+ return predicate.User(sql.FieldGTE(FieldCreatedAt, v))
}
// CreatedAtLT applies the LT predicate on the "created_at" field.
func CreatedAtLT(v time.Time) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LT(s.C(FieldCreatedAt), v))
- })
+ return predicate.User(sql.FieldLT(FieldCreatedAt, v))
}
// CreatedAtLTE applies the LTE predicate on the "created_at" field.
func CreatedAtLTE(v time.Time) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s.Where(sql.LTE(s.C(FieldCreatedAt), v))
- })
+ return predicate.User(sql.FieldLTE(FieldCreatedAt, v))
}
// HasOwner applies the HasEdge predicate on the "owner" edge.
@@ -535,7 +345,6 @@ func HasOwner() predicate.User {
return predicate.User(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
- sqlgraph.To(OwnerTable, FieldID),
sqlgraph.Edge(sqlgraph.O2M, true, OwnerTable, OwnerColumn),
)
sqlgraph.HasNeighbors(s, step)
@@ -545,11 +354,7 @@ func HasOwner() predicate.User {
// HasOwnerWith applies the HasEdge predicate on the "owner" edge with a given conditions (other predicates).
func HasOwnerWith(preds ...predicate.PasswordToken) predicate.User {
return predicate.User(func(s *sql.Selector) {
- step := sqlgraph.NewStep(
- sqlgraph.From(Table, FieldID),
- sqlgraph.To(OwnerInverseTable, FieldID),
- sqlgraph.Edge(sqlgraph.O2M, true, OwnerTable, OwnerColumn),
- )
+ step := newOwnerStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
@@ -560,32 +365,15 @@ func HasOwnerWith(preds ...predicate.PasswordToken) predicate.User {
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.User) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s1 := s.Clone().SetP(nil)
- for _, p := range predicates {
- p(s1)
- }
- s.Where(s1.P())
- })
+ return predicate.User(sql.AndPredicates(predicates...))
}
// Or groups predicates with the OR operator between them.
func Or(predicates ...predicate.User) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- s1 := s.Clone().SetP(nil)
- for i, p := range predicates {
- if i > 0 {
- s1.Or()
- }
- p(s1)
- }
- s.Where(s1.P())
- })
+ return predicate.User(sql.OrPredicates(predicates...))
}
// Not applies the not operator on the given predicate.
func Not(p predicate.User) predicate.User {
- return predicate.User(func(s *sql.Selector) {
- p(s.Not())
- })
+ return predicate.User(sql.NotPredicates(p))
}
diff --git a/ent/user_create.go b/ent/user_create.go
index ca26043..ae14177 100644
--- a/ent/user_create.go
+++ b/ent/user_create.go
@@ -1,4 +1,4 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
@@ -6,12 +6,12 @@ import (
"context"
"errors"
"fmt"
- "goweb/ent/passwordtoken"
- "goweb/ent/user"
"time"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/user"
)
// UserCreate is the builder for creating a User entity.
@@ -22,104 +22,96 @@ type UserCreate struct {
}
// SetName sets the "name" field.
-func (uc *UserCreate) SetName(s string) *UserCreate {
- uc.mutation.SetName(s)
- return uc
+func (_c *UserCreate) SetName(v string) *UserCreate {
+ _c.mutation.SetName(v)
+ return _c
}
// SetEmail sets the "email" field.
-func (uc *UserCreate) SetEmail(s string) *UserCreate {
- uc.mutation.SetEmail(s)
- return uc
+func (_c *UserCreate) SetEmail(v string) *UserCreate {
+ _c.mutation.SetEmail(v)
+ return _c
}
// SetPassword sets the "password" field.
-func (uc *UserCreate) SetPassword(s string) *UserCreate {
- uc.mutation.SetPassword(s)
- return uc
+func (_c *UserCreate) SetPassword(v string) *UserCreate {
+ _c.mutation.SetPassword(v)
+ return _c
+}
+
+// SetVerified sets the "verified" field.
+func (_c *UserCreate) SetVerified(v bool) *UserCreate {
+ _c.mutation.SetVerified(v)
+ return _c
+}
+
+// SetNillableVerified sets the "verified" field if the given value is not nil.
+func (_c *UserCreate) SetNillableVerified(v *bool) *UserCreate {
+ if v != nil {
+ _c.SetVerified(*v)
+ }
+ return _c
+}
+
+// SetAdmin sets the "admin" field.
+func (_c *UserCreate) SetAdmin(v bool) *UserCreate {
+ _c.mutation.SetAdmin(v)
+ return _c
+}
+
+// SetNillableAdmin sets the "admin" field if the given value is not nil.
+func (_c *UserCreate) SetNillableAdmin(v *bool) *UserCreate {
+ if v != nil {
+ _c.SetAdmin(*v)
+ }
+ return _c
}
// SetCreatedAt sets the "created_at" field.
-func (uc *UserCreate) SetCreatedAt(t time.Time) *UserCreate {
- uc.mutation.SetCreatedAt(t)
- return uc
+func (_c *UserCreate) SetCreatedAt(v time.Time) *UserCreate {
+ _c.mutation.SetCreatedAt(v)
+ return _c
}
// SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
-func (uc *UserCreate) SetNillableCreatedAt(t *time.Time) *UserCreate {
- if t != nil {
- uc.SetCreatedAt(*t)
+func (_c *UserCreate) SetNillableCreatedAt(v *time.Time) *UserCreate {
+ if v != nil {
+ _c.SetCreatedAt(*v)
}
- return uc
+ return _c
}
// AddOwnerIDs adds the "owner" edge to the PasswordToken entity by IDs.
-func (uc *UserCreate) AddOwnerIDs(ids ...int) *UserCreate {
- uc.mutation.AddOwnerIDs(ids...)
- return uc
+func (_c *UserCreate) AddOwnerIDs(ids ...int) *UserCreate {
+ _c.mutation.AddOwnerIDs(ids...)
+ return _c
}
// AddOwner adds the "owner" edges to the PasswordToken entity.
-func (uc *UserCreate) AddOwner(p ...*PasswordToken) *UserCreate {
- ids := make([]int, len(p))
- for i := range p {
- ids[i] = p[i].ID
+func (_c *UserCreate) AddOwner(v ...*PasswordToken) *UserCreate {
+ ids := make([]int, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
}
- return uc.AddOwnerIDs(ids...)
+ return _c.AddOwnerIDs(ids...)
}
// Mutation returns the UserMutation object of the builder.
-func (uc *UserCreate) Mutation() *UserMutation {
- return uc.mutation
+func (_c *UserCreate) Mutation() *UserMutation {
+ return _c.mutation
}
// Save creates the User in the database.
-func (uc *UserCreate) Save(ctx context.Context) (*User, error) {
- var (
- err error
- node *User
- )
- if err := uc.defaults(); err != nil {
+func (_c *UserCreate) Save(ctx context.Context) (*User, error) {
+ if err := _c.defaults(); err != nil {
return nil, err
}
- if len(uc.hooks) == 0 {
- if err = uc.check(); err != nil {
- return nil, err
- }
- node, err = uc.sqlSave(ctx)
- } else {
- var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
- mutation, ok := m.(*UserMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T", m)
- }
- if err = uc.check(); err != nil {
- return nil, err
- }
- uc.mutation = mutation
- if node, err = uc.sqlSave(ctx); err != nil {
- return nil, err
- }
- mutation.id = &node.ID
- mutation.done = true
- return node, err
- })
- for i := len(uc.hooks) - 1; i >= 0; i-- {
- if uc.hooks[i] == nil {
- return nil, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)")
- }
- mut = uc.hooks[i](mut)
- }
- if _, err := mut.Mutate(ctx, uc.mutation); err != nil {
- return nil, err
- }
- }
- return node, err
+ return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks)
}
// SaveX calls Save and panics if Save returns an error.
-func (uc *UserCreate) SaveX(ctx context.Context) *User {
- v, err := uc.Save(ctx)
+func (_c *UserCreate) SaveX(ctx context.Context) *User {
+ v, err := _c.Save(ctx)
if err != nil {
panic(err)
}
@@ -127,119 +119,124 @@ func (uc *UserCreate) SaveX(ctx context.Context) *User {
}
// Exec executes the query.
-func (uc *UserCreate) Exec(ctx context.Context) error {
- _, err := uc.Save(ctx)
+func (_c *UserCreate) Exec(ctx context.Context) error {
+ _, err := _c.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
-func (uc *UserCreate) ExecX(ctx context.Context) {
- if err := uc.Exec(ctx); err != nil {
+func (_c *UserCreate) ExecX(ctx context.Context) {
+ if err := _c.Exec(ctx); err != nil {
panic(err)
}
}
// defaults sets the default values of the builder before save.
-func (uc *UserCreate) defaults() error {
- if _, ok := uc.mutation.CreatedAt(); !ok {
+func (_c *UserCreate) defaults() error {
+ if _, ok := _c.mutation.Verified(); !ok {
+ v := user.DefaultVerified
+ _c.mutation.SetVerified(v)
+ }
+ if _, ok := _c.mutation.Admin(); !ok {
+ v := user.DefaultAdmin
+ _c.mutation.SetAdmin(v)
+ }
+ if _, ok := _c.mutation.CreatedAt(); !ok {
if user.DefaultCreatedAt == nil {
return fmt.Errorf("ent: uninitialized user.DefaultCreatedAt (forgotten import ent/runtime?)")
}
v := user.DefaultCreatedAt()
- uc.mutation.SetCreatedAt(v)
+ _c.mutation.SetCreatedAt(v)
}
return nil
}
// check runs all checks and user-defined validators on the builder.
-func (uc *UserCreate) check() error {
- if _, ok := uc.mutation.Name(); !ok {
- return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "name"`)}
+func (_c *UserCreate) check() error {
+ if _, ok := _c.mutation.Name(); !ok {
+ return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "User.name"`)}
}
- if v, ok := uc.mutation.Name(); ok {
+ if v, ok := _c.mutation.Name(); ok {
if err := user.NameValidator(v); err != nil {
- return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "name": %w`, err)}
+ return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "User.name": %w`, err)}
}
}
- if _, ok := uc.mutation.Email(); !ok {
- return &ValidationError{Name: "email", err: errors.New(`ent: missing required field "email"`)}
+ if _, ok := _c.mutation.Email(); !ok {
+ return &ValidationError{Name: "email", err: errors.New(`ent: missing required field "User.email"`)}
}
- if v, ok := uc.mutation.Email(); ok {
+ if v, ok := _c.mutation.Email(); ok {
if err := user.EmailValidator(v); err != nil {
- return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "email": %w`, err)}
+ return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "User.email": %w`, err)}
}
}
- if _, ok := uc.mutation.Password(); !ok {
- return &ValidationError{Name: "password", err: errors.New(`ent: missing required field "password"`)}
+ if _, ok := _c.mutation.Password(); !ok {
+ return &ValidationError{Name: "password", err: errors.New(`ent: missing required field "User.password"`)}
}
- if v, ok := uc.mutation.Password(); ok {
+ if v, ok := _c.mutation.Password(); ok {
if err := user.PasswordValidator(v); err != nil {
- return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "password": %w`, err)}
+ return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "User.password": %w`, err)}
}
}
- if _, ok := uc.mutation.CreatedAt(); !ok {
- return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "created_at"`)}
+ if _, ok := _c.mutation.Verified(); !ok {
+ return &ValidationError{Name: "verified", err: errors.New(`ent: missing required field "User.verified"`)}
+ }
+ if _, ok := _c.mutation.Admin(); !ok {
+ return &ValidationError{Name: "admin", err: errors.New(`ent: missing required field "User.admin"`)}
+ }
+ if _, ok := _c.mutation.CreatedAt(); !ok {
+ return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "User.created_at"`)}
}
return nil
}
-func (uc *UserCreate) sqlSave(ctx context.Context) (*User, error) {
- _node, _spec := uc.createSpec()
- if err := sqlgraph.CreateNode(ctx, uc.driver, _spec); err != nil {
+func (_c *UserCreate) sqlSave(ctx context.Context) (*User, error) {
+ if err := _c.check(); err != nil {
+ return nil, err
+ }
+ _node, _spec := _c.createSpec()
+ if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil {
if sqlgraph.IsConstraintError(err) {
- err = &ConstraintError{err.Error(), err}
+ err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
id := _spec.ID.Value.(int64)
_node.ID = int(id)
+ _c.mutation.id = &_node.ID
+ _c.mutation.done = true
return _node, nil
}
-func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
+func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
var (
- _node = &User{config: uc.config}
- _spec = &sqlgraph.CreateSpec{
- Table: user.Table,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
- }
+ _node = &User{config: _c.config}
+ _spec = sqlgraph.NewCreateSpec(user.Table, sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt))
)
- if value, ok := uc.mutation.Name(); ok {
- _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: user.FieldName,
- })
+ if value, ok := _c.mutation.Name(); ok {
+ _spec.SetField(user.FieldName, field.TypeString, value)
_node.Name = value
}
- if value, ok := uc.mutation.Email(); ok {
- _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: user.FieldEmail,
- })
+ if value, ok := _c.mutation.Email(); ok {
+ _spec.SetField(user.FieldEmail, field.TypeString, value)
_node.Email = value
}
- if value, ok := uc.mutation.Password(); ok {
- _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: user.FieldPassword,
- })
+ if value, ok := _c.mutation.Password(); ok {
+ _spec.SetField(user.FieldPassword, field.TypeString, value)
_node.Password = value
}
- if value, ok := uc.mutation.CreatedAt(); ok {
- _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
- Type: field.TypeTime,
- Value: value,
- Column: user.FieldCreatedAt,
- })
+ if value, ok := _c.mutation.Verified(); ok {
+ _spec.SetField(user.FieldVerified, field.TypeBool, value)
+ _node.Verified = value
+ }
+ if value, ok := _c.mutation.Admin(); ok {
+ _spec.SetField(user.FieldAdmin, field.TypeBool, value)
+ _node.Admin = value
+ }
+ if value, ok := _c.mutation.CreatedAt(); ok {
+ _spec.SetField(user.FieldCreatedAt, field.TypeTime, value)
_node.CreatedAt = value
}
- if nodes := uc.mutation.OwnerIDs(); len(nodes) > 0 {
+ if nodes := _c.mutation.OwnerIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
@@ -247,10 +244,7 @@ func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
Columns: []string{user.OwnerColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt),
},
}
for _, k := range nodes {
@@ -264,17 +258,21 @@ func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
// UserCreateBulk is the builder for creating many User entities in bulk.
type UserCreateBulk struct {
config
+ err error
builders []*UserCreate
}
// Save creates the User entities in the database.
-func (ucb *UserCreateBulk) Save(ctx context.Context) ([]*User, error) {
- specs := make([]*sqlgraph.CreateSpec, len(ucb.builders))
- nodes := make([]*User, len(ucb.builders))
- mutators := make([]Mutator, len(ucb.builders))
- for i := range ucb.builders {
+func (_c *UserCreateBulk) Save(ctx context.Context) ([]*User, error) {
+ if _c.err != nil {
+ return nil, _c.err
+ }
+ specs := make([]*sqlgraph.CreateSpec, len(_c.builders))
+ nodes := make([]*User, len(_c.builders))
+ mutators := make([]Mutator, len(_c.builders))
+ for i := range _c.builders {
func(i int, root context.Context) {
- builder := ucb.builders[i]
+ builder := _c.builders[i]
builder.defaults()
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
mutation, ok := m.(*UserMutation)
@@ -285,16 +283,16 @@ func (ucb *UserCreateBulk) Save(ctx context.Context) ([]*User, error) {
return nil, err
}
builder.mutation = mutation
- nodes[i], specs[i] = builder.createSpec()
var err error
+ nodes[i], specs[i] = builder.createSpec()
if i < len(mutators)-1 {
- _, err = mutators[i+1].Mutate(root, ucb.builders[i+1].mutation)
+ _, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation)
} else {
spec := &sqlgraph.BatchCreateSpec{Nodes: specs}
// Invoke the actual operation on the latest mutation in the chain.
- if err = sqlgraph.BatchCreate(ctx, ucb.driver, spec); err != nil {
+ if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil {
if sqlgraph.IsConstraintError(err) {
- err = &ConstraintError{err.Error(), err}
+ err = &ConstraintError{msg: err.Error(), wrap: err}
}
}
}
@@ -302,11 +300,11 @@ func (ucb *UserCreateBulk) Save(ctx context.Context) ([]*User, error) {
return nil, err
}
mutation.id = &nodes[i].ID
- mutation.done = true
if specs[i].ID.Value != nil {
id := specs[i].ID.Value.(int64)
nodes[i].ID = int(id)
}
+ mutation.done = true
return nodes[i], nil
})
for i := len(builder.hooks) - 1; i >= 0; i-- {
@@ -316,7 +314,7 @@ func (ucb *UserCreateBulk) Save(ctx context.Context) ([]*User, error) {
}(i, ctx)
}
if len(mutators) > 0 {
- if _, err := mutators[0].Mutate(ctx, ucb.builders[0].mutation); err != nil {
+ if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil {
return nil, err
}
}
@@ -324,8 +322,8 @@ func (ucb *UserCreateBulk) Save(ctx context.Context) ([]*User, error) {
}
// SaveX is like Save, but panics if an error occurs.
-func (ucb *UserCreateBulk) SaveX(ctx context.Context) []*User {
- v, err := ucb.Save(ctx)
+func (_c *UserCreateBulk) SaveX(ctx context.Context) []*User {
+ v, err := _c.Save(ctx)
if err != nil {
panic(err)
}
@@ -333,14 +331,14 @@ func (ucb *UserCreateBulk) SaveX(ctx context.Context) []*User {
}
// Exec executes the query.
-func (ucb *UserCreateBulk) Exec(ctx context.Context) error {
- _, err := ucb.Save(ctx)
+func (_c *UserCreateBulk) Exec(ctx context.Context) error {
+ _, err := _c.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
-func (ucb *UserCreateBulk) ExecX(ctx context.Context) {
- if err := ucb.Exec(ctx); err != nil {
+func (_c *UserCreateBulk) ExecX(ctx context.Context) {
+ if err := _c.Exec(ctx); err != nil {
panic(err)
}
}
diff --git a/ent/user_delete.go b/ent/user_delete.go
index 2f5e308..4a51d5a 100644
--- a/ent/user_delete.go
+++ b/ent/user_delete.go
@@ -1,16 +1,15 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
- "fmt"
- "goweb/ent/predicate"
- "goweb/ent/user"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
+ "github.com/mikestefanello/pagoda/ent/predicate"
+ "github.com/mikestefanello/pagoda/ent/user"
)
// UserDelete is the builder for deleting a User entity.
@@ -21,80 +20,56 @@ type UserDelete struct {
}
// Where appends a list predicates to the UserDelete builder.
-func (ud *UserDelete) Where(ps ...predicate.User) *UserDelete {
- ud.mutation.Where(ps...)
- return ud
+func (_d *UserDelete) Where(ps ...predicate.User) *UserDelete {
+ _d.mutation.Where(ps...)
+ return _d
}
// Exec executes the deletion query and returns how many vertices were deleted.
-func (ud *UserDelete) Exec(ctx context.Context) (int, error) {
- var (
- err error
- affected int
- )
- if len(ud.hooks) == 0 {
- affected, err = ud.sqlExec(ctx)
- } else {
- var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
- mutation, ok := m.(*UserMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T", m)
- }
- ud.mutation = mutation
- affected, err = ud.sqlExec(ctx)
- mutation.done = true
- return affected, err
- })
- for i := len(ud.hooks) - 1; i >= 0; i-- {
- if ud.hooks[i] == nil {
- return 0, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)")
- }
- mut = ud.hooks[i](mut)
- }
- if _, err := mut.Mutate(ctx, ud.mutation); err != nil {
- return 0, err
- }
- }
- return affected, err
+func (_d *UserDelete) Exec(ctx context.Context) (int, error) {
+ return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
}
// ExecX is like Exec, but panics if an error occurs.
-func (ud *UserDelete) ExecX(ctx context.Context) int {
- n, err := ud.Exec(ctx)
+func (_d *UserDelete) ExecX(ctx context.Context) int {
+ n, err := _d.Exec(ctx)
if err != nil {
panic(err)
}
return n
}
-func (ud *UserDelete) sqlExec(ctx context.Context) (int, error) {
- _spec := &sqlgraph.DeleteSpec{
- Node: &sqlgraph.NodeSpec{
- Table: user.Table,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
- },
- }
- if ps := ud.mutation.predicates; len(ps) > 0 {
+func (_d *UserDelete) sqlExec(ctx context.Context) (int, error) {
+ _spec := sqlgraph.NewDeleteSpec(user.Table, sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt))
+ if ps := _d.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
- return sqlgraph.DeleteNodes(ctx, ud.driver, _spec)
+ affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
+ if err != nil && sqlgraph.IsConstraintError(err) {
+ err = &ConstraintError{msg: err.Error(), wrap: err}
+ }
+ _d.mutation.done = true
+ return affected, err
}
// UserDeleteOne is the builder for deleting a single User entity.
type UserDeleteOne struct {
- ud *UserDelete
+ _d *UserDelete
+}
+
+// Where appends a list predicates to the UserDelete builder.
+func (_d *UserDeleteOne) Where(ps ...predicate.User) *UserDeleteOne {
+ _d._d.mutation.Where(ps...)
+ return _d
}
// Exec executes the deletion query.
-func (udo *UserDeleteOne) Exec(ctx context.Context) error {
- n, err := udo.ud.Exec(ctx)
+func (_d *UserDeleteOne) Exec(ctx context.Context) error {
+ n, err := _d._d.Exec(ctx)
switch {
case err != nil:
return err
@@ -106,6 +81,8 @@ func (udo *UserDeleteOne) Exec(ctx context.Context) error {
}
// ExecX is like Exec, but panics if an error occurs.
-func (udo *UserDeleteOne) ExecX(ctx context.Context) {
- udo.ud.ExecX(ctx)
+func (_d *UserDeleteOne) ExecX(ctx context.Context) {
+ if err := _d.Exec(ctx); err != nil {
+ panic(err)
+ }
}
diff --git a/ent/user_query.go b/ent/user_query.go
index c955ecc..7aedb58 100644
--- a/ent/user_query.go
+++ b/ent/user_query.go
@@ -1,77 +1,74 @@
-// Code generated by entc, DO NOT EDIT.
+// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"database/sql/driver"
- "errors"
"fmt"
- "goweb/ent/passwordtoken"
- "goweb/ent/predicate"
- "goweb/ent/user"
"math"
+ "entgo.io/ent"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/predicate"
+ "github.com/mikestefanello/pagoda/ent/user"
)
// UserQuery is the builder for querying User entities.
type UserQuery struct {
config
- limit *int
- offset *int
- unique *bool
- order []OrderFunc
- fields []string
+ ctx *QueryContext
+ order []user.OrderOption
+ inters []Interceptor
predicates []predicate.User
- // eager-loading edges.
- withOwner *PasswordTokenQuery
+ withOwner *PasswordTokenQuery
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
}
// Where adds a new predicate for the UserQuery builder.
-func (uq *UserQuery) Where(ps ...predicate.User) *UserQuery {
- uq.predicates = append(uq.predicates, ps...)
- return uq
+func (_q *UserQuery) Where(ps ...predicate.User) *UserQuery {
+ _q.predicates = append(_q.predicates, ps...)
+ return _q
}
-// Limit adds a limit step to the query.
-func (uq *UserQuery) Limit(limit int) *UserQuery {
- uq.limit = &limit
- return uq
+// Limit the number of records to be returned by this query.
+func (_q *UserQuery) Limit(limit int) *UserQuery {
+ _q.ctx.Limit = &limit
+ return _q
}
-// Offset adds an offset step to the query.
-func (uq *UserQuery) Offset(offset int) *UserQuery {
- uq.offset = &offset
- return uq
+// Offset to start from.
+func (_q *UserQuery) Offset(offset int) *UserQuery {
+ _q.ctx.Offset = &offset
+ return _q
}
// Unique configures the query builder to filter duplicate records on query.
// By default, unique is set to true, and can be disabled using this method.
-func (uq *UserQuery) Unique(unique bool) *UserQuery {
- uq.unique = &unique
- return uq
+func (_q *UserQuery) Unique(unique bool) *UserQuery {
+ _q.ctx.Unique = &unique
+ return _q
}
-// Order adds an order step to the query.
-func (uq *UserQuery) Order(o ...OrderFunc) *UserQuery {
- uq.order = append(uq.order, o...)
- return uq
+// Order specifies how the records should be ordered.
+func (_q *UserQuery) Order(o ...user.OrderOption) *UserQuery {
+ _q.order = append(_q.order, o...)
+ return _q
}
// QueryOwner chains the current query on the "owner" edge.
-func (uq *UserQuery) QueryOwner() *PasswordTokenQuery {
- query := &PasswordTokenQuery{config: uq.config}
+func (_q *UserQuery) QueryOwner() *PasswordTokenQuery {
+ query := (&PasswordTokenClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
- if err := uq.prepareQuery(ctx); err != nil {
+ if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
- selector := uq.sqlQuery(ctx)
+ selector := _q.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
@@ -80,7 +77,7 @@ func (uq *UserQuery) QueryOwner() *PasswordTokenQuery {
sqlgraph.To(passwordtoken.Table, passwordtoken.FieldID),
sqlgraph.Edge(sqlgraph.O2M, true, user.OwnerTable, user.OwnerColumn),
)
- fromU = sqlgraph.SetNeighbors(uq.driver.Dialect(), step)
+ fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
}
return query
@@ -88,8 +85,8 @@ func (uq *UserQuery) QueryOwner() *PasswordTokenQuery {
// First returns the first User entity from the query.
// Returns a *NotFoundError when no User was found.
-func (uq *UserQuery) First(ctx context.Context) (*User, error) {
- nodes, err := uq.Limit(1).All(ctx)
+func (_q *UserQuery) First(ctx context.Context) (*User, error) {
+ nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst))
if err != nil {
return nil, err
}
@@ -100,8 +97,8 @@ func (uq *UserQuery) First(ctx context.Context) (*User, error) {
}
// FirstX is like First, but panics if an error occurs.
-func (uq *UserQuery) FirstX(ctx context.Context) *User {
- node, err := uq.First(ctx)
+func (_q *UserQuery) FirstX(ctx context.Context) *User {
+ node, err := _q.First(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
@@ -110,9 +107,9 @@ func (uq *UserQuery) FirstX(ctx context.Context) *User {
// FirstID returns the first User ID from the query.
// Returns a *NotFoundError when no User ID was found.
-func (uq *UserQuery) FirstID(ctx context.Context) (id int, err error) {
+func (_q *UserQuery) FirstID(ctx context.Context) (id int, err error) {
var ids []int
- if ids, err = uq.Limit(1).IDs(ctx); err != nil {
+ if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil {
return
}
if len(ids) == 0 {
@@ -123,8 +120,8 @@ func (uq *UserQuery) FirstID(ctx context.Context) (id int, err error) {
}
// FirstIDX is like FirstID, but panics if an error occurs.
-func (uq *UserQuery) FirstIDX(ctx context.Context) int {
- id, err := uq.FirstID(ctx)
+func (_q *UserQuery) FirstIDX(ctx context.Context) int {
+ id, err := _q.FirstID(ctx)
if err != nil && !IsNotFound(err) {
panic(err)
}
@@ -132,10 +129,10 @@ func (uq *UserQuery) FirstIDX(ctx context.Context) int {
}
// Only returns a single User entity found by the query, ensuring it only returns one.
-// Returns a *NotSingularError when exactly one User entity is not found.
+// Returns a *NotSingularError when more than one User entity is found.
// Returns a *NotFoundError when no User entities are found.
-func (uq *UserQuery) Only(ctx context.Context) (*User, error) {
- nodes, err := uq.Limit(2).All(ctx)
+func (_q *UserQuery) Only(ctx context.Context) (*User, error) {
+ nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly))
if err != nil {
return nil, err
}
@@ -150,8 +147,8 @@ func (uq *UserQuery) Only(ctx context.Context) (*User, error) {
}
// OnlyX is like Only, but panics if an error occurs.
-func (uq *UserQuery) OnlyX(ctx context.Context) *User {
- node, err := uq.Only(ctx)
+func (_q *UserQuery) OnlyX(ctx context.Context) *User {
+ node, err := _q.Only(ctx)
if err != nil {
panic(err)
}
@@ -159,11 +156,11 @@ func (uq *UserQuery) OnlyX(ctx context.Context) *User {
}
// OnlyID is like Only, but returns the only User ID in the query.
-// Returns a *NotSingularError when exactly one User ID is not found.
+// Returns a *NotSingularError when more than one User ID is found.
// Returns a *NotFoundError when no entities are found.
-func (uq *UserQuery) OnlyID(ctx context.Context) (id int, err error) {
+func (_q *UserQuery) OnlyID(ctx context.Context) (id int, err error) {
var ids []int
- if ids, err = uq.Limit(2).IDs(ctx); err != nil {
+ if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil {
return
}
switch len(ids) {
@@ -178,8 +175,8 @@ func (uq *UserQuery) OnlyID(ctx context.Context) (id int, err error) {
}
// OnlyIDX is like OnlyID, but panics if an error occurs.
-func (uq *UserQuery) OnlyIDX(ctx context.Context) int {
- id, err := uq.OnlyID(ctx)
+func (_q *UserQuery) OnlyIDX(ctx context.Context) int {
+ id, err := _q.OnlyID(ctx)
if err != nil {
panic(err)
}
@@ -187,16 +184,18 @@ func (uq *UserQuery) OnlyIDX(ctx context.Context) int {
}
// All executes the query and returns a list of Users.
-func (uq *UserQuery) All(ctx context.Context) ([]*User, error) {
- if err := uq.prepareQuery(ctx); err != nil {
+func (_q *UserQuery) All(ctx context.Context) ([]*User, error) {
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll)
+ if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
- return uq.sqlAll(ctx)
+ qr := querierAll[[]*User, *UserQuery]()
+ return withInterceptors[[]*User](ctx, _q, qr, _q.inters)
}
// AllX is like All, but panics if an error occurs.
-func (uq *UserQuery) AllX(ctx context.Context) []*User {
- nodes, err := uq.All(ctx)
+func (_q *UserQuery) AllX(ctx context.Context) []*User {
+ nodes, err := _q.All(ctx)
if err != nil {
panic(err)
}
@@ -204,17 +203,20 @@ func (uq *UserQuery) AllX(ctx context.Context) []*User {
}
// IDs executes the query and returns a list of User IDs.
-func (uq *UserQuery) IDs(ctx context.Context) ([]int, error) {
- var ids []int
- if err := uq.Select(user.FieldID).Scan(ctx, &ids); err != nil {
+func (_q *UserQuery) IDs(ctx context.Context) (ids []int, err error) {
+ if _q.ctx.Unique == nil && _q.path != nil {
+ _q.Unique(true)
+ }
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs)
+ if err = _q.Select(user.FieldID).Scan(ctx, &ids); err != nil {
return nil, err
}
return ids, nil
}
// IDsX is like IDs, but panics if an error occurs.
-func (uq *UserQuery) IDsX(ctx context.Context) []int {
- ids, err := uq.IDs(ctx)
+func (_q *UserQuery) IDsX(ctx context.Context) []int {
+ ids, err := _q.IDs(ctx)
if err != nil {
panic(err)
}
@@ -222,16 +224,17 @@ func (uq *UserQuery) IDsX(ctx context.Context) []int {
}
// Count returns the count of the given query.
-func (uq *UserQuery) Count(ctx context.Context) (int, error) {
- if err := uq.prepareQuery(ctx); err != nil {
+func (_q *UserQuery) Count(ctx context.Context) (int, error) {
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount)
+ if err := _q.prepareQuery(ctx); err != nil {
return 0, err
}
- return uq.sqlCount(ctx)
+ return withInterceptors[int](ctx, _q, querierCount[*UserQuery](), _q.inters)
}
// CountX is like Count, but panics if an error occurs.
-func (uq *UserQuery) CountX(ctx context.Context) int {
- count, err := uq.Count(ctx)
+func (_q *UserQuery) CountX(ctx context.Context) int {
+ count, err := _q.Count(ctx)
if err != nil {
panic(err)
}
@@ -239,16 +242,21 @@ func (uq *UserQuery) CountX(ctx context.Context) int {
}
// Exist returns true if the query has elements in the graph.
-func (uq *UserQuery) Exist(ctx context.Context) (bool, error) {
- if err := uq.prepareQuery(ctx); err != nil {
- return false, err
+func (_q *UserQuery) Exist(ctx context.Context) (bool, error) {
+ ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist)
+ switch _, err := _q.FirstID(ctx); {
+ case IsNotFound(err):
+ return false, nil
+ case err != nil:
+ return false, fmt.Errorf("ent: check existence: %w", err)
+ default:
+ return true, nil
}
- return uq.sqlExist(ctx)
}
// ExistX is like Exist, but panics if an error occurs.
-func (uq *UserQuery) ExistX(ctx context.Context) bool {
- exist, err := uq.Exist(ctx)
+func (_q *UserQuery) ExistX(ctx context.Context) bool {
+ exist, err := _q.Exist(ctx)
if err != nil {
panic(err)
}
@@ -257,32 +265,32 @@ func (uq *UserQuery) ExistX(ctx context.Context) bool {
// Clone returns a duplicate of the UserQuery builder, including all associated steps. It can be
// used to prepare common query builders and use them differently after the clone is made.
-func (uq *UserQuery) Clone() *UserQuery {
- if uq == nil {
+func (_q *UserQuery) Clone() *UserQuery {
+ if _q == nil {
return nil
}
return &UserQuery{
- config: uq.config,
- limit: uq.limit,
- offset: uq.offset,
- order: append([]OrderFunc{}, uq.order...),
- predicates: append([]predicate.User{}, uq.predicates...),
- withOwner: uq.withOwner.Clone(),
+ config: _q.config,
+ ctx: _q.ctx.Clone(),
+ order: append([]user.OrderOption{}, _q.order...),
+ inters: append([]Interceptor{}, _q.inters...),
+ predicates: append([]predicate.User{}, _q.predicates...),
+ withOwner: _q.withOwner.Clone(),
// clone intermediate query.
- sql: uq.sql.Clone(),
- path: uq.path,
+ sql: _q.sql.Clone(),
+ path: _q.path,
}
}
// WithOwner tells the query-builder to eager-load the nodes that are connected to
// the "owner" edge. The optional arguments are used to configure the query builder of the edge.
-func (uq *UserQuery) WithOwner(opts ...func(*PasswordTokenQuery)) *UserQuery {
- query := &PasswordTokenQuery{config: uq.config}
+func (_q *UserQuery) WithOwner(opts ...func(*PasswordTokenQuery)) *UserQuery {
+ query := (&PasswordTokenClient{config: _q.config}).Query()
for _, opt := range opts {
opt(query)
}
- uq.withOwner = query
- return uq
+ _q.withOwner = query
+ return _q
}
// GroupBy is used to group vertices by one or more fields/columns.
@@ -299,17 +307,13 @@ func (uq *UserQuery) WithOwner(opts ...func(*PasswordTokenQuery)) *UserQuery {
// GroupBy(user.FieldName).
// Aggregate(ent.Count()).
// Scan(ctx, &v)
-//
-func (uq *UserQuery) GroupBy(field string, fields ...string) *UserGroupBy {
- group := &UserGroupBy{config: uq.config}
- group.fields = append([]string{field}, fields...)
- group.path = func(ctx context.Context) (prev *sql.Selector, err error) {
- if err := uq.prepareQuery(ctx); err != nil {
- return nil, err
- }
- return uq.sqlQuery(ctx), nil
- }
- return group
+func (_q *UserQuery) GroupBy(field string, fields ...string) *UserGroupBy {
+ _q.ctx.Fields = append([]string{field}, fields...)
+ grbuild := &UserGroupBy{build: _q}
+ grbuild.flds = &_q.ctx.Fields
+ grbuild.label = user.Label
+ grbuild.scan = grbuild.Scan
+ return grbuild
}
// Select allows the selection one or more fields/columns for the given query,
@@ -324,118 +328,130 @@ func (uq *UserQuery) GroupBy(field string, fields ...string) *UserGroupBy {
// client.User.Query().
// Select(user.FieldName).
// Scan(ctx, &v)
-//
-func (uq *UserQuery) Select(fields ...string) *UserSelect {
- uq.fields = append(uq.fields, fields...)
- return &UserSelect{UserQuery: uq}
+func (_q *UserQuery) Select(fields ...string) *UserSelect {
+ _q.ctx.Fields = append(_q.ctx.Fields, fields...)
+ sbuild := &UserSelect{UserQuery: _q}
+ sbuild.label = user.Label
+ sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan
+ return sbuild
}
-func (uq *UserQuery) prepareQuery(ctx context.Context) error {
- for _, f := range uq.fields {
+// Aggregate returns a UserSelect configured with the given aggregations.
+func (_q *UserQuery) Aggregate(fns ...AggregateFunc) *UserSelect {
+ return _q.Select().Aggregate(fns...)
+}
+
+func (_q *UserQuery) prepareQuery(ctx context.Context) error {
+ for _, inter := range _q.inters {
+ if inter == nil {
+ return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
+ }
+ if trv, ok := inter.(Traverser); ok {
+ if err := trv.Traverse(ctx, _q); err != nil {
+ return err
+ }
+ }
+ }
+ for _, f := range _q.ctx.Fields {
if !user.ValidColumn(f) {
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
}
}
- if uq.path != nil {
- prev, err := uq.path(ctx)
+ if _q.path != nil {
+ prev, err := _q.path(ctx)
if err != nil {
return err
}
- uq.sql = prev
+ _q.sql = prev
}
return nil
}
-func (uq *UserQuery) sqlAll(ctx context.Context) ([]*User, error) {
+func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, error) {
var (
nodes = []*User{}
- _spec = uq.querySpec()
+ _spec = _q.querySpec()
loadedTypes = [1]bool{
- uq.withOwner != nil,
+ _q.withOwner != nil,
}
)
- _spec.ScanValues = func(columns []string) ([]interface{}, error) {
- node := &User{config: uq.config}
- nodes = append(nodes, node)
- return node.scanValues(columns)
+ _spec.ScanValues = func(columns []string) ([]any, error) {
+ return (*User).scanValues(nil, columns)
}
- _spec.Assign = func(columns []string, values []interface{}) error {
- if len(nodes) == 0 {
- return fmt.Errorf("ent: Assign called without calling ScanValues")
- }
- node := nodes[len(nodes)-1]
+ _spec.Assign = func(columns []string, values []any) error {
+ node := &User{config: _q.config}
+ nodes = append(nodes, node)
node.Edges.loadedTypes = loadedTypes
return node.assignValues(columns, values)
}
- if err := sqlgraph.QueryNodes(ctx, uq.driver, _spec); err != nil {
+ for i := range hooks {
+ hooks[i](ctx, _spec)
+ }
+ if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil {
return nil, err
}
if len(nodes) == 0 {
return nodes, nil
}
-
- if query := uq.withOwner; query != nil {
- fks := make([]driver.Value, 0, len(nodes))
- nodeids := make(map[int]*User)
- for i := range nodes {
- fks = append(fks, nodes[i].ID)
- nodeids[nodes[i].ID] = nodes[i]
- nodes[i].Edges.Owner = []*PasswordToken{}
- }
- query.withFKs = true
- query.Where(predicate.PasswordToken(func(s *sql.Selector) {
- s.Where(sql.InValues(user.OwnerColumn, fks...))
- }))
- neighbors, err := query.All(ctx)
- if err != nil {
+ if query := _q.withOwner; query != nil {
+ if err := _q.loadOwner(ctx, query, nodes,
+ func(n *User) { n.Edges.Owner = []*PasswordToken{} },
+ func(n *User, e *PasswordToken) { n.Edges.Owner = append(n.Edges.Owner, e) }); err != nil {
return nil, err
}
- for _, n := range neighbors {
- fk := n.password_token_user
- if fk == nil {
- return nil, fmt.Errorf(`foreign-key "password_token_user" is nil for node %v`, n.ID)
- }
- node, ok := nodeids[*fk]
- if !ok {
- return nil, fmt.Errorf(`unexpected foreign-key "password_token_user" returned %v for node %v`, *fk, n.ID)
- }
- node.Edges.Owner = append(node.Edges.Owner, n)
- }
}
-
return nodes, nil
}
-func (uq *UserQuery) sqlCount(ctx context.Context) (int, error) {
- _spec := uq.querySpec()
- return sqlgraph.CountNodes(ctx, uq.driver, _spec)
-}
-
-func (uq *UserQuery) sqlExist(ctx context.Context) (bool, error) {
- n, err := uq.sqlCount(ctx)
+func (_q *UserQuery) loadOwner(ctx context.Context, query *PasswordTokenQuery, nodes []*User, init func(*User), assign func(*User, *PasswordToken)) error {
+ fks := make([]driver.Value, 0, len(nodes))
+ nodeids := make(map[int]*User)
+ for i := range nodes {
+ fks = append(fks, nodes[i].ID)
+ nodeids[nodes[i].ID] = nodes[i]
+ if init != nil {
+ init(nodes[i])
+ }
+ }
+ if len(query.ctx.Fields) > 0 {
+ query.ctx.AppendFieldOnce(passwordtoken.FieldUserID)
+ }
+ query.Where(predicate.PasswordToken(func(s *sql.Selector) {
+ s.Where(sql.InValues(s.C(user.OwnerColumn), fks...))
+ }))
+ neighbors, err := query.All(ctx)
if err != nil {
- return false, fmt.Errorf("ent: check existence: %w", err)
+ return err
}
- return n > 0, nil
+ for _, n := range neighbors {
+ fk := n.UserID
+ node, ok := nodeids[fk]
+ if !ok {
+ return fmt.Errorf(`unexpected referenced foreign-key "user_id" returned %v for node %v`, fk, n.ID)
+ }
+ assign(node, n)
+ }
+ return nil
}
-func (uq *UserQuery) querySpec() *sqlgraph.QuerySpec {
- _spec := &sqlgraph.QuerySpec{
- Node: &sqlgraph.NodeSpec{
- Table: user.Table,
- Columns: user.Columns,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
- },
- From: uq.sql,
- Unique: true,
+func (_q *UserQuery) sqlCount(ctx context.Context) (int, error) {
+ _spec := _q.querySpec()
+ _spec.Node.Columns = _q.ctx.Fields
+ if len(_q.ctx.Fields) > 0 {
+ _spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
}
- if unique := uq.unique; unique != nil {
+ return sqlgraph.CountNodes(ctx, _q.driver, _spec)
+}
+
+func (_q *UserQuery) querySpec() *sqlgraph.QuerySpec {
+ _spec := sqlgraph.NewQuerySpec(user.Table, user.Columns, sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt))
+ _spec.From = _q.sql
+ if unique := _q.ctx.Unique; unique != nil {
_spec.Unique = *unique
+ } else if _q.path != nil {
+ _spec.Unique = true
}
- if fields := uq.fields; len(fields) > 0 {
+ if fields := _q.ctx.Fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, user.FieldID)
for i := range fields {
@@ -444,20 +460,20 @@ func (uq *UserQuery) querySpec() *sqlgraph.QuerySpec {
}
}
}
- if ps := uq.predicates; len(ps) > 0 {
+ if ps := _q.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
- if limit := uq.limit; limit != nil {
+ if limit := _q.ctx.Limit; limit != nil {
_spec.Limit = *limit
}
- if offset := uq.offset; offset != nil {
+ if offset := _q.ctx.Offset; offset != nil {
_spec.Offset = *offset
}
- if ps := uq.order; len(ps) > 0 {
+ if ps := _q.order; len(ps) > 0 {
_spec.Order = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
@@ -467,30 +483,33 @@ func (uq *UserQuery) querySpec() *sqlgraph.QuerySpec {
return _spec
}
-func (uq *UserQuery) sqlQuery(ctx context.Context) *sql.Selector {
- builder := sql.Dialect(uq.driver.Dialect())
+func (_q *UserQuery) sqlQuery(ctx context.Context) *sql.Selector {
+ builder := sql.Dialect(_q.driver.Dialect())
t1 := builder.Table(user.Table)
- columns := uq.fields
+ columns := _q.ctx.Fields
if len(columns) == 0 {
columns = user.Columns
}
selector := builder.Select(t1.Columns(columns...)...).From(t1)
- if uq.sql != nil {
- selector = uq.sql
+ if _q.sql != nil {
+ selector = _q.sql
selector.Select(selector.Columns(columns...)...)
}
- for _, p := range uq.predicates {
+ if _q.ctx.Unique != nil && *_q.ctx.Unique {
+ selector.Distinct()
+ }
+ for _, p := range _q.predicates {
p(selector)
}
- for _, p := range uq.order {
+ for _, p := range _q.order {
p(selector)
}
- if offset := uq.offset; offset != nil {
+ if offset := _q.ctx.Offset; offset != nil {
// limit is mandatory for offset clause. We start
// with default value, and override it below if needed.
selector.Offset(*offset).Limit(math.MaxInt32)
}
- if limit := uq.limit; limit != nil {
+ if limit := _q.ctx.Limit; limit != nil {
selector.Limit(*limit)
}
return selector
@@ -498,488 +517,88 @@ func (uq *UserQuery) sqlQuery(ctx context.Context) *sql.Selector {
// UserGroupBy is the group-by builder for User entities.
type UserGroupBy struct {
- config
- fields []string
- fns []AggregateFunc
- // intermediate query (i.e. traversal path).
- sql *sql.Selector
- path func(context.Context) (*sql.Selector, error)
+ selector
+ build *UserQuery
}
// Aggregate adds the given aggregation functions to the group-by query.
-func (ugb *UserGroupBy) Aggregate(fns ...AggregateFunc) *UserGroupBy {
- ugb.fns = append(ugb.fns, fns...)
- return ugb
+func (_g *UserGroupBy) Aggregate(fns ...AggregateFunc) *UserGroupBy {
+ _g.fns = append(_g.fns, fns...)
+ return _g
}
-// Scan applies the group-by query and scans the result into the given value.
-func (ugb *UserGroupBy) Scan(ctx context.Context, v interface{}) error {
- query, err := ugb.path(ctx)
- if err != nil {
+// Scan applies the selector query and scans the result into the given value.
+func (_g *UserGroupBy) Scan(ctx context.Context, v any) error {
+ ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy)
+ if err := _g.build.prepareQuery(ctx); err != nil {
return err
}
- ugb.sql = query
- return ugb.sqlScan(ctx, v)
+ return scanWithInterceptors[*UserQuery, *UserGroupBy](ctx, _g.build, _g, _g.build.inters, v)
}
-// ScanX is like Scan, but panics if an error occurs.
-func (ugb *UserGroupBy) ScanX(ctx context.Context, v interface{}) {
- if err := ugb.Scan(ctx, v); err != nil {
- panic(err)
+func (_g *UserGroupBy) sqlScan(ctx context.Context, root *UserQuery, v any) error {
+ selector := root.sqlQuery(ctx).Select()
+ aggregation := make([]string, 0, len(_g.fns))
+ for _, fn := range _g.fns {
+ aggregation = append(aggregation, fn(selector))
}
-}
-
-// Strings returns list of strings from group-by.
-// It is only allowed when executing a group-by query with one field.
-func (ugb *UserGroupBy) Strings(ctx context.Context) ([]string, error) {
- if len(ugb.fields) > 1 {
- return nil, errors.New("ent: UserGroupBy.Strings is not achievable when grouping more than 1 field")
- }
- var v []string
- if err := ugb.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// StringsX is like Strings, but panics if an error occurs.
-func (ugb *UserGroupBy) StringsX(ctx context.Context) []string {
- v, err := ugb.Strings(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// String returns a single string from a group-by query.
-// It is only allowed when executing a group-by query with one field.
-func (ugb *UserGroupBy) String(ctx context.Context) (_ string, err error) {
- var v []string
- if v, err = ugb.Strings(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{user.Label}
- default:
- err = fmt.Errorf("ent: UserGroupBy.Strings returned %d results when one was expected", len(v))
- }
- return
-}
-
-// StringX is like String, but panics if an error occurs.
-func (ugb *UserGroupBy) StringX(ctx context.Context) string {
- v, err := ugb.String(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Ints returns list of ints from group-by.
-// It is only allowed when executing a group-by query with one field.
-func (ugb *UserGroupBy) Ints(ctx context.Context) ([]int, error) {
- if len(ugb.fields) > 1 {
- return nil, errors.New("ent: UserGroupBy.Ints is not achievable when grouping more than 1 field")
- }
- var v []int
- if err := ugb.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// IntsX is like Ints, but panics if an error occurs.
-func (ugb *UserGroupBy) IntsX(ctx context.Context) []int {
- v, err := ugb.Ints(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Int returns a single int from a group-by query.
-// It is only allowed when executing a group-by query with one field.
-func (ugb *UserGroupBy) Int(ctx context.Context) (_ int, err error) {
- var v []int
- if v, err = ugb.Ints(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{user.Label}
- default:
- err = fmt.Errorf("ent: UserGroupBy.Ints returned %d results when one was expected", len(v))
- }
- return
-}
-
-// IntX is like Int, but panics if an error occurs.
-func (ugb *UserGroupBy) IntX(ctx context.Context) int {
- v, err := ugb.Int(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Float64s returns list of float64s from group-by.
-// It is only allowed when executing a group-by query with one field.
-func (ugb *UserGroupBy) Float64s(ctx context.Context) ([]float64, error) {
- if len(ugb.fields) > 1 {
- return nil, errors.New("ent: UserGroupBy.Float64s is not achievable when grouping more than 1 field")
- }
- var v []float64
- if err := ugb.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// Float64sX is like Float64s, but panics if an error occurs.
-func (ugb *UserGroupBy) Float64sX(ctx context.Context) []float64 {
- v, err := ugb.Float64s(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Float64 returns a single float64 from a group-by query.
-// It is only allowed when executing a group-by query with one field.
-func (ugb *UserGroupBy) Float64(ctx context.Context) (_ float64, err error) {
- var v []float64
- if v, err = ugb.Float64s(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{user.Label}
- default:
- err = fmt.Errorf("ent: UserGroupBy.Float64s returned %d results when one was expected", len(v))
- }
- return
-}
-
-// Float64X is like Float64, but panics if an error occurs.
-func (ugb *UserGroupBy) Float64X(ctx context.Context) float64 {
- v, err := ugb.Float64(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Bools returns list of bools from group-by.
-// It is only allowed when executing a group-by query with one field.
-func (ugb *UserGroupBy) Bools(ctx context.Context) ([]bool, error) {
- if len(ugb.fields) > 1 {
- return nil, errors.New("ent: UserGroupBy.Bools is not achievable when grouping more than 1 field")
- }
- var v []bool
- if err := ugb.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// BoolsX is like Bools, but panics if an error occurs.
-func (ugb *UserGroupBy) BoolsX(ctx context.Context) []bool {
- v, err := ugb.Bools(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-// Bool returns a single bool from a group-by query.
-// It is only allowed when executing a group-by query with one field.
-func (ugb *UserGroupBy) Bool(ctx context.Context) (_ bool, err error) {
- var v []bool
- if v, err = ugb.Bools(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{user.Label}
- default:
- err = fmt.Errorf("ent: UserGroupBy.Bools returned %d results when one was expected", len(v))
- }
- return
-}
-
-// BoolX is like Bool, but panics if an error occurs.
-func (ugb *UserGroupBy) BoolX(ctx context.Context) bool {
- v, err := ugb.Bool(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-func (ugb *UserGroupBy) sqlScan(ctx context.Context, v interface{}) error {
- for _, f := range ugb.fields {
- if !user.ValidColumn(f) {
- return &ValidationError{Name: f, err: fmt.Errorf("invalid field %q for group-by", f)}
+ if len(selector.SelectedColumns()) == 0 {
+ columns := make([]string, 0, len(*_g.flds)+len(_g.fns))
+ for _, f := range *_g.flds {
+ columns = append(columns, selector.C(f))
}
+ columns = append(columns, aggregation...)
+ selector.Select(columns...)
}
- selector := ugb.sqlQuery()
+ selector.GroupBy(selector.Columns(*_g.flds...)...)
if err := selector.Err(); err != nil {
return err
}
rows := &sql.Rows{}
query, args := selector.Query()
- if err := ugb.driver.Query(ctx, query, args, rows); err != nil {
+ if err := _g.build.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}
-func (ugb *UserGroupBy) sqlQuery() *sql.Selector {
- selector := ugb.sql.Select()
- aggregation := make([]string, 0, len(ugb.fns))
- for _, fn := range ugb.fns {
- aggregation = append(aggregation, fn(selector))
- }
- // If no columns were selected in a custom aggregation function, the default
- // selection is the fields used for "group-by", and the aggregation functions.
- if len(selector.SelectedColumns()) == 0 {
- columns := make([]string, 0, len(ugb.fields)+len(ugb.fns))
- for _, f := range ugb.fields {
- columns = append(columns, selector.C(f))
- }
- for _, c := range aggregation {
- columns = append(columns, c)
- }
- selector.Select(columns...)
- }
- return selector.GroupBy(selector.Columns(ugb.fields...)...)
-}
-
// UserSelect is the builder for selecting fields of User entities.
type UserSelect struct {
*UserQuery
- // intermediate query (i.e. traversal path).
- sql *sql.Selector
+ selector
+}
+
+// Aggregate adds the given aggregation functions to the selector query.
+func (_s *UserSelect) Aggregate(fns ...AggregateFunc) *UserSelect {
+ _s.fns = append(_s.fns, fns...)
+ return _s
}
// Scan applies the selector query and scans the result into the given value.
-func (us *UserSelect) Scan(ctx context.Context, v interface{}) error {
- if err := us.prepareQuery(ctx); err != nil {
+func (_s *UserSelect) Scan(ctx context.Context, v any) error {
+ ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect)
+ if err := _s.prepareQuery(ctx); err != nil {
return err
}
- us.sql = us.UserQuery.sqlQuery(ctx)
- return us.sqlScan(ctx, v)
+ return scanWithInterceptors[*UserQuery, *UserSelect](ctx, _s.UserQuery, _s, _s.inters, v)
}
-// ScanX is like Scan, but panics if an error occurs.
-func (us *UserSelect) ScanX(ctx context.Context, v interface{}) {
- if err := us.Scan(ctx, v); err != nil {
- panic(err)
+func (_s *UserSelect) sqlScan(ctx context.Context, root *UserQuery, v any) error {
+ selector := root.sqlQuery(ctx)
+ aggregation := make([]string, 0, len(_s.fns))
+ for _, fn := range _s.fns {
+ aggregation = append(aggregation, fn(selector))
}
-}
-
-// Strings returns list of strings from a selector. It is only allowed when selecting one field.
-func (us *UserSelect) Strings(ctx context.Context) ([]string, error) {
- if len(us.fields) > 1 {
- return nil, errors.New("ent: UserSelect.Strings is not achievable when selecting more than 1 field")
+ switch n := len(*_s.selector.flds); {
+ case n == 0 && len(aggregation) > 0:
+ selector.Select(aggregation...)
+ case n != 0 && len(aggregation) > 0:
+ selector.AppendSelect(aggregation...)
}
- var v []string
- if err := us.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// StringsX is like Strings, but panics if an error occurs.
-func (us *UserSelect) StringsX(ctx context.Context) []string {
- v, err := us.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 (us *UserSelect) String(ctx context.Context) (_ string, err error) {
- var v []string
- if v, err = us.Strings(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{user.Label}
- default:
- err = fmt.Errorf("ent: UserSelect.Strings returned %d results when one was expected", len(v))
- }
- return
-}
-
-// StringX is like String, but panics if an error occurs.
-func (us *UserSelect) StringX(ctx context.Context) string {
- v, err := us.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 (us *UserSelect) Ints(ctx context.Context) ([]int, error) {
- if len(us.fields) > 1 {
- return nil, errors.New("ent: UserSelect.Ints is not achievable when selecting more than 1 field")
- }
- var v []int
- if err := us.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// IntsX is like Ints, but panics if an error occurs.
-func (us *UserSelect) IntsX(ctx context.Context) []int {
- v, err := us.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 (us *UserSelect) Int(ctx context.Context) (_ int, err error) {
- var v []int
- if v, err = us.Ints(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{user.Label}
- default:
- err = fmt.Errorf("ent: UserSelect.Ints returned %d results when one was expected", len(v))
- }
- return
-}
-
-// IntX is like Int, but panics if an error occurs.
-func (us *UserSelect) IntX(ctx context.Context) int {
- v, err := us.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 (us *UserSelect) Float64s(ctx context.Context) ([]float64, error) {
- if len(us.fields) > 1 {
- return nil, errors.New("ent: UserSelect.Float64s is not achievable when selecting more than 1 field")
- }
- var v []float64
- if err := us.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// Float64sX is like Float64s, but panics if an error occurs.
-func (us *UserSelect) Float64sX(ctx context.Context) []float64 {
- v, err := us.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 (us *UserSelect) Float64(ctx context.Context) (_ float64, err error) {
- var v []float64
- if v, err = us.Float64s(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{user.Label}
- default:
- err = fmt.Errorf("ent: UserSelect.Float64s returned %d results when one was expected", len(v))
- }
- return
-}
-
-// Float64X is like Float64, but panics if an error occurs.
-func (us *UserSelect) Float64X(ctx context.Context) float64 {
- v, err := us.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 (us *UserSelect) Bools(ctx context.Context) ([]bool, error) {
- if len(us.fields) > 1 {
- return nil, errors.New("ent: UserSelect.Bools is not achievable when selecting more than 1 field")
- }
- var v []bool
- if err := us.Scan(ctx, &v); err != nil {
- return nil, err
- }
- return v, nil
-}
-
-// BoolsX is like Bools, but panics if an error occurs.
-func (us *UserSelect) BoolsX(ctx context.Context) []bool {
- v, err := us.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 (us *UserSelect) Bool(ctx context.Context) (_ bool, err error) {
- var v []bool
- if v, err = us.Bools(ctx); err != nil {
- return
- }
- switch len(v) {
- case 1:
- return v[0], nil
- case 0:
- err = &NotFoundError{user.Label}
- default:
- err = fmt.Errorf("ent: UserSelect.Bools returned %d results when one was expected", len(v))
- }
- return
-}
-
-// BoolX is like Bool, but panics if an error occurs.
-func (us *UserSelect) BoolX(ctx context.Context) bool {
- v, err := us.Bool(ctx)
- if err != nil {
- panic(err)
- }
- return v
-}
-
-func (us *UserSelect) sqlScan(ctx context.Context, v interface{}) error {
rows := &sql.Rows{}
- query, args := us.sql.Query()
- if err := us.driver.Query(ctx, query, args, rows); err != nil {
+ query, args := selector.Query()
+ if err := _s.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
diff --git a/ent/user_update.go b/ent/user_update.go
index bedec7a..cda7bda 100644
--- a/ent/user_update.go
+++ b/ent/user_update.go
@@ -1,17 +1,18 @@
-// 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"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/schema/field"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/predicate"
+ "github.com/mikestefanello/pagoda/ent/user"
)
// UserUpdate is the builder for updating User entities.
@@ -22,111 +23,130 @@ type UserUpdate struct {
}
// Where appends a list predicates to the UserUpdate builder.
-func (uu *UserUpdate) Where(ps ...predicate.User) *UserUpdate {
- uu.mutation.Where(ps...)
- return uu
+func (_u *UserUpdate) Where(ps ...predicate.User) *UserUpdate {
+ _u.mutation.Where(ps...)
+ return _u
}
// SetName sets the "name" field.
-func (uu *UserUpdate) SetName(s string) *UserUpdate {
- uu.mutation.SetName(s)
- return uu
+func (_u *UserUpdate) SetName(v string) *UserUpdate {
+ _u.mutation.SetName(v)
+ return _u
+}
+
+// SetNillableName sets the "name" field if the given value is not nil.
+func (_u *UserUpdate) SetNillableName(v *string) *UserUpdate {
+ if v != nil {
+ _u.SetName(*v)
+ }
+ return _u
}
// SetEmail sets the "email" field.
-func (uu *UserUpdate) SetEmail(s string) *UserUpdate {
- uu.mutation.SetEmail(s)
- return uu
+func (_u *UserUpdate) SetEmail(v string) *UserUpdate {
+ _u.mutation.SetEmail(v)
+ return _u
+}
+
+// SetNillableEmail sets the "email" field if the given value is not nil.
+func (_u *UserUpdate) SetNillableEmail(v *string) *UserUpdate {
+ if v != nil {
+ _u.SetEmail(*v)
+ }
+ return _u
}
// SetPassword sets the "password" field.
-func (uu *UserUpdate) SetPassword(s string) *UserUpdate {
- uu.mutation.SetPassword(s)
- return uu
+func (_u *UserUpdate) SetPassword(v string) *UserUpdate {
+ _u.mutation.SetPassword(v)
+ return _u
+}
+
+// SetNillablePassword sets the "password" field if the given value is not nil.
+func (_u *UserUpdate) SetNillablePassword(v *string) *UserUpdate {
+ if v != nil {
+ _u.SetPassword(*v)
+ }
+ return _u
+}
+
+// SetVerified sets the "verified" field.
+func (_u *UserUpdate) SetVerified(v bool) *UserUpdate {
+ _u.mutation.SetVerified(v)
+ return _u
+}
+
+// SetNillableVerified sets the "verified" field if the given value is not nil.
+func (_u *UserUpdate) SetNillableVerified(v *bool) *UserUpdate {
+ if v != nil {
+ _u.SetVerified(*v)
+ }
+ return _u
+}
+
+// SetAdmin sets the "admin" field.
+func (_u *UserUpdate) SetAdmin(v bool) *UserUpdate {
+ _u.mutation.SetAdmin(v)
+ return _u
+}
+
+// SetNillableAdmin sets the "admin" field if the given value is not nil.
+func (_u *UserUpdate) SetNillableAdmin(v *bool) *UserUpdate {
+ if v != nil {
+ _u.SetAdmin(*v)
+ }
+ return _u
}
// AddOwnerIDs adds the "owner" edge to the PasswordToken entity by IDs.
-func (uu *UserUpdate) AddOwnerIDs(ids ...int) *UserUpdate {
- uu.mutation.AddOwnerIDs(ids...)
- return uu
+func (_u *UserUpdate) AddOwnerIDs(ids ...int) *UserUpdate {
+ _u.mutation.AddOwnerIDs(ids...)
+ return _u
}
// AddOwner adds the "owner" edges to the PasswordToken entity.
-func (uu *UserUpdate) AddOwner(p ...*PasswordToken) *UserUpdate {
- ids := make([]int, len(p))
- for i := range p {
- ids[i] = p[i].ID
+func (_u *UserUpdate) AddOwner(v ...*PasswordToken) *UserUpdate {
+ ids := make([]int, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
}
- return uu.AddOwnerIDs(ids...)
+ return _u.AddOwnerIDs(ids...)
}
// Mutation returns the UserMutation object of the builder.
-func (uu *UserUpdate) Mutation() *UserMutation {
- return uu.mutation
+func (_u *UserUpdate) Mutation() *UserMutation {
+ return _u.mutation
}
// ClearOwner clears all "owner" edges to the PasswordToken entity.
-func (uu *UserUpdate) ClearOwner() *UserUpdate {
- uu.mutation.ClearOwner()
- return uu
+func (_u *UserUpdate) ClearOwner() *UserUpdate {
+ _u.mutation.ClearOwner()
+ return _u
}
// RemoveOwnerIDs removes the "owner" edge to PasswordToken entities by IDs.
-func (uu *UserUpdate) RemoveOwnerIDs(ids ...int) *UserUpdate {
- uu.mutation.RemoveOwnerIDs(ids...)
- return uu
+func (_u *UserUpdate) RemoveOwnerIDs(ids ...int) *UserUpdate {
+ _u.mutation.RemoveOwnerIDs(ids...)
+ return _u
}
// RemoveOwner removes "owner" edges to PasswordToken entities.
-func (uu *UserUpdate) RemoveOwner(p ...*PasswordToken) *UserUpdate {
- ids := make([]int, len(p))
- for i := range p {
- ids[i] = p[i].ID
+func (_u *UserUpdate) RemoveOwner(v ...*PasswordToken) *UserUpdate {
+ ids := make([]int, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
}
- return uu.RemoveOwnerIDs(ids...)
+ return _u.RemoveOwnerIDs(ids...)
}
// Save executes the query and returns the number of nodes affected by the update operation.
-func (uu *UserUpdate) Save(ctx context.Context) (int, error) {
- var (
- err error
- affected int
- )
- if len(uu.hooks) == 0 {
- if err = uu.check(); err != nil {
- return 0, err
- }
- affected, err = uu.sqlSave(ctx)
- } else {
- var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
- mutation, ok := m.(*UserMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T", m)
- }
- if err = uu.check(); err != nil {
- return 0, err
- }
- uu.mutation = mutation
- affected, err = uu.sqlSave(ctx)
- mutation.done = true
- return affected, err
- })
- for i := len(uu.hooks) - 1; i >= 0; i-- {
- if uu.hooks[i] == nil {
- return 0, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)")
- }
- mut = uu.hooks[i](mut)
- }
- if _, err := mut.Mutate(ctx, uu.mutation); err != nil {
- return 0, err
- }
- }
- return affected, err
+func (_u *UserUpdate) Save(ctx context.Context) (int, error) {
+ return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
}
// SaveX is like Save, but panics if an error occurs.
-func (uu *UserUpdate) SaveX(ctx context.Context) int {
- affected, err := uu.Save(ctx)
+func (_u *UserUpdate) SaveX(ctx context.Context) int {
+ affected, err := _u.Save(ctx)
if err != nil {
panic(err)
}
@@ -134,78 +154,66 @@ func (uu *UserUpdate) SaveX(ctx context.Context) int {
}
// Exec executes the query.
-func (uu *UserUpdate) Exec(ctx context.Context) error {
- _, err := uu.Save(ctx)
+func (_u *UserUpdate) Exec(ctx context.Context) error {
+ _, err := _u.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
-func (uu *UserUpdate) ExecX(ctx context.Context) {
- if err := uu.Exec(ctx); err != nil {
+func (_u *UserUpdate) ExecX(ctx context.Context) {
+ if err := _u.Exec(ctx); err != nil {
panic(err)
}
}
// check runs all checks and user-defined validators on the builder.
-func (uu *UserUpdate) check() error {
- if v, ok := uu.mutation.Name(); ok {
+func (_u *UserUpdate) check() error {
+ if v, ok := _u.mutation.Name(); ok {
if err := user.NameValidator(v); err != nil {
- return &ValidationError{Name: "name", err: fmt.Errorf("ent: validator failed for field \"name\": %w", err)}
+ return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "User.name": %w`, err)}
}
}
- if v, ok := uu.mutation.Email(); ok {
+ if v, ok := _u.mutation.Email(); ok {
if err := user.EmailValidator(v); err != nil {
- return &ValidationError{Name: "email", err: fmt.Errorf("ent: validator failed for field \"email\": %w", err)}
+ return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "User.email": %w`, err)}
}
}
- if v, ok := uu.mutation.Password(); ok {
+ if v, ok := _u.mutation.Password(); ok {
if err := user.PasswordValidator(v); err != nil {
- return &ValidationError{Name: "password", err: fmt.Errorf("ent: validator failed for field \"password\": %w", err)}
+ return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "User.password": %w`, err)}
}
}
return nil
}
-func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) {
- _spec := &sqlgraph.UpdateSpec{
- Node: &sqlgraph.NodeSpec{
- Table: user.Table,
- Columns: user.Columns,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
- },
+func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
+ if err := _u.check(); err != nil {
+ return _node, err
}
- if ps := uu.mutation.predicates; len(ps) > 0 {
+ _spec := sqlgraph.NewUpdateSpec(user.Table, user.Columns, sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt))
+ if ps := _u.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
- if value, ok := uu.mutation.Name(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: user.FieldName,
- })
+ if value, ok := _u.mutation.Name(); ok {
+ _spec.SetField(user.FieldName, field.TypeString, value)
}
- if value, ok := uu.mutation.Email(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: user.FieldEmail,
- })
+ if value, ok := _u.mutation.Email(); ok {
+ _spec.SetField(user.FieldEmail, field.TypeString, value)
}
- if value, ok := uu.mutation.Password(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: user.FieldPassword,
- })
+ if value, ok := _u.mutation.Password(); ok {
+ _spec.SetField(user.FieldPassword, field.TypeString, value)
}
- if uu.mutation.OwnerCleared() {
+ if value, ok := _u.mutation.Verified(); ok {
+ _spec.SetField(user.FieldVerified, field.TypeBool, value)
+ }
+ if value, ok := _u.mutation.Admin(); ok {
+ _spec.SetField(user.FieldAdmin, field.TypeBool, value)
+ }
+ if _u.mutation.OwnerCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
@@ -213,15 +221,12 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{user.OwnerColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
- if nodes := uu.mutation.RemovedOwnerIDs(); len(nodes) > 0 && !uu.mutation.OwnerCleared() {
+ if nodes := _u.mutation.RemovedOwnerIDs(); len(nodes) > 0 && !_u.mutation.OwnerCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
@@ -229,10 +234,7 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{user.OwnerColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt),
},
}
for _, k := range nodes {
@@ -240,7 +242,7 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) {
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
- if nodes := uu.mutation.OwnerIDs(); len(nodes) > 0 {
+ if nodes := _u.mutation.OwnerIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
@@ -248,10 +250,7 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{user.OwnerColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt),
},
}
for _, k := range nodes {
@@ -259,15 +258,16 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) {
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
- if n, err = sqlgraph.UpdateNodes(ctx, uu.driver, _spec); err != nil {
+ if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{user.Label}
} else if sqlgraph.IsConstraintError(err) {
- err = &ConstraintError{err.Error(), err}
+ err = &ConstraintError{msg: err.Error(), wrap: err}
}
return 0, err
}
- return n, nil
+ _u.mutation.done = true
+ return _node, nil
}
// UserUpdateOne is the builder for updating a single User entity.
@@ -279,112 +279,137 @@ type UserUpdateOne struct {
}
// SetName sets the "name" field.
-func (uuo *UserUpdateOne) SetName(s string) *UserUpdateOne {
- uuo.mutation.SetName(s)
- return uuo
+func (_u *UserUpdateOne) SetName(v string) *UserUpdateOne {
+ _u.mutation.SetName(v)
+ return _u
+}
+
+// SetNillableName sets the "name" field if the given value is not nil.
+func (_u *UserUpdateOne) SetNillableName(v *string) *UserUpdateOne {
+ if v != nil {
+ _u.SetName(*v)
+ }
+ return _u
}
// SetEmail sets the "email" field.
-func (uuo *UserUpdateOne) SetEmail(s string) *UserUpdateOne {
- uuo.mutation.SetEmail(s)
- return uuo
+func (_u *UserUpdateOne) SetEmail(v string) *UserUpdateOne {
+ _u.mutation.SetEmail(v)
+ return _u
+}
+
+// SetNillableEmail sets the "email" field if the given value is not nil.
+func (_u *UserUpdateOne) SetNillableEmail(v *string) *UserUpdateOne {
+ if v != nil {
+ _u.SetEmail(*v)
+ }
+ return _u
}
// SetPassword sets the "password" field.
-func (uuo *UserUpdateOne) SetPassword(s string) *UserUpdateOne {
- uuo.mutation.SetPassword(s)
- return uuo
+func (_u *UserUpdateOne) SetPassword(v string) *UserUpdateOne {
+ _u.mutation.SetPassword(v)
+ return _u
+}
+
+// SetNillablePassword sets the "password" field if the given value is not nil.
+func (_u *UserUpdateOne) SetNillablePassword(v *string) *UserUpdateOne {
+ if v != nil {
+ _u.SetPassword(*v)
+ }
+ return _u
+}
+
+// SetVerified sets the "verified" field.
+func (_u *UserUpdateOne) SetVerified(v bool) *UserUpdateOne {
+ _u.mutation.SetVerified(v)
+ return _u
+}
+
+// SetNillableVerified sets the "verified" field if the given value is not nil.
+func (_u *UserUpdateOne) SetNillableVerified(v *bool) *UserUpdateOne {
+ if v != nil {
+ _u.SetVerified(*v)
+ }
+ return _u
+}
+
+// SetAdmin sets the "admin" field.
+func (_u *UserUpdateOne) SetAdmin(v bool) *UserUpdateOne {
+ _u.mutation.SetAdmin(v)
+ return _u
+}
+
+// SetNillableAdmin sets the "admin" field if the given value is not nil.
+func (_u *UserUpdateOne) SetNillableAdmin(v *bool) *UserUpdateOne {
+ if v != nil {
+ _u.SetAdmin(*v)
+ }
+ return _u
}
// AddOwnerIDs adds the "owner" edge to the PasswordToken entity by IDs.
-func (uuo *UserUpdateOne) AddOwnerIDs(ids ...int) *UserUpdateOne {
- uuo.mutation.AddOwnerIDs(ids...)
- return uuo
+func (_u *UserUpdateOne) AddOwnerIDs(ids ...int) *UserUpdateOne {
+ _u.mutation.AddOwnerIDs(ids...)
+ return _u
}
// AddOwner adds the "owner" edges to the PasswordToken entity.
-func (uuo *UserUpdateOne) AddOwner(p ...*PasswordToken) *UserUpdateOne {
- ids := make([]int, len(p))
- for i := range p {
- ids[i] = p[i].ID
+func (_u *UserUpdateOne) AddOwner(v ...*PasswordToken) *UserUpdateOne {
+ ids := make([]int, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
}
- return uuo.AddOwnerIDs(ids...)
+ return _u.AddOwnerIDs(ids...)
}
// Mutation returns the UserMutation object of the builder.
-func (uuo *UserUpdateOne) Mutation() *UserMutation {
- return uuo.mutation
+func (_u *UserUpdateOne) Mutation() *UserMutation {
+ return _u.mutation
}
// ClearOwner clears all "owner" edges to the PasswordToken entity.
-func (uuo *UserUpdateOne) ClearOwner() *UserUpdateOne {
- uuo.mutation.ClearOwner()
- return uuo
+func (_u *UserUpdateOne) ClearOwner() *UserUpdateOne {
+ _u.mutation.ClearOwner()
+ return _u
}
// RemoveOwnerIDs removes the "owner" edge to PasswordToken entities by IDs.
-func (uuo *UserUpdateOne) RemoveOwnerIDs(ids ...int) *UserUpdateOne {
- uuo.mutation.RemoveOwnerIDs(ids...)
- return uuo
+func (_u *UserUpdateOne) RemoveOwnerIDs(ids ...int) *UserUpdateOne {
+ _u.mutation.RemoveOwnerIDs(ids...)
+ return _u
}
// RemoveOwner removes "owner" edges to PasswordToken entities.
-func (uuo *UserUpdateOne) RemoveOwner(p ...*PasswordToken) *UserUpdateOne {
- ids := make([]int, len(p))
- for i := range p {
- ids[i] = p[i].ID
+func (_u *UserUpdateOne) RemoveOwner(v ...*PasswordToken) *UserUpdateOne {
+ ids := make([]int, len(v))
+ for i := range v {
+ ids[i] = v[i].ID
}
- return uuo.RemoveOwnerIDs(ids...)
+ return _u.RemoveOwnerIDs(ids...)
+}
+
+// Where appends a list predicates to the UserUpdate builder.
+func (_u *UserUpdateOne) Where(ps ...predicate.User) *UserUpdateOne {
+ _u.mutation.Where(ps...)
+ return _u
}
// Select allows selecting one or more fields (columns) of the returned entity.
// The default is selecting all fields defined in the entity schema.
-func (uuo *UserUpdateOne) Select(field string, fields ...string) *UserUpdateOne {
- uuo.fields = append([]string{field}, fields...)
- return uuo
+func (_u *UserUpdateOne) Select(field string, fields ...string) *UserUpdateOne {
+ _u.fields = append([]string{field}, fields...)
+ return _u
}
// Save executes the query and returns the updated User entity.
-func (uuo *UserUpdateOne) Save(ctx context.Context) (*User, error) {
- var (
- err error
- node *User
- )
- if len(uuo.hooks) == 0 {
- if err = uuo.check(); err != nil {
- return nil, err
- }
- node, err = uuo.sqlSave(ctx)
- } else {
- var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
- mutation, ok := m.(*UserMutation)
- if !ok {
- return nil, fmt.Errorf("unexpected mutation type %T", m)
- }
- if err = uuo.check(); err != nil {
- return nil, err
- }
- uuo.mutation = mutation
- node, err = uuo.sqlSave(ctx)
- mutation.done = true
- return node, err
- })
- for i := len(uuo.hooks) - 1; i >= 0; i-- {
- if uuo.hooks[i] == nil {
- return nil, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)")
- }
- mut = uuo.hooks[i](mut)
- }
- if _, err := mut.Mutate(ctx, uuo.mutation); err != nil {
- return nil, err
- }
- }
- return node, err
+func (_u *UserUpdateOne) Save(ctx context.Context) (*User, error) {
+ return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
}
// SaveX is like Save, but panics if an error occurs.
-func (uuo *UserUpdateOne) SaveX(ctx context.Context) *User {
- node, err := uuo.Save(ctx)
+func (_u *UserUpdateOne) SaveX(ctx context.Context) *User {
+ node, err := _u.Save(ctx)
if err != nil {
panic(err)
}
@@ -392,55 +417,49 @@ func (uuo *UserUpdateOne) SaveX(ctx context.Context) *User {
}
// Exec executes the query on the entity.
-func (uuo *UserUpdateOne) Exec(ctx context.Context) error {
- _, err := uuo.Save(ctx)
+func (_u *UserUpdateOne) Exec(ctx context.Context) error {
+ _, err := _u.Save(ctx)
return err
}
// ExecX is like Exec, but panics if an error occurs.
-func (uuo *UserUpdateOne) ExecX(ctx context.Context) {
- if err := uuo.Exec(ctx); err != nil {
+func (_u *UserUpdateOne) ExecX(ctx context.Context) {
+ if err := _u.Exec(ctx); err != nil {
panic(err)
}
}
// check runs all checks and user-defined validators on the builder.
-func (uuo *UserUpdateOne) check() error {
- if v, ok := uuo.mutation.Name(); ok {
+func (_u *UserUpdateOne) check() error {
+ if v, ok := _u.mutation.Name(); ok {
if err := user.NameValidator(v); err != nil {
- return &ValidationError{Name: "name", err: fmt.Errorf("ent: validator failed for field \"name\": %w", err)}
+ return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "User.name": %w`, err)}
}
}
- if v, ok := uuo.mutation.Email(); ok {
+ if v, ok := _u.mutation.Email(); ok {
if err := user.EmailValidator(v); err != nil {
- return &ValidationError{Name: "email", err: fmt.Errorf("ent: validator failed for field \"email\": %w", err)}
+ return &ValidationError{Name: "email", err: fmt.Errorf(`ent: validator failed for field "User.email": %w`, err)}
}
}
- if v, ok := uuo.mutation.Password(); ok {
+ if v, ok := _u.mutation.Password(); ok {
if err := user.PasswordValidator(v); err != nil {
- return &ValidationError{Name: "password", err: fmt.Errorf("ent: validator failed for field \"password\": %w", err)}
+ return &ValidationError{Name: "password", err: fmt.Errorf(`ent: validator failed for field "User.password": %w`, err)}
}
}
return nil
}
-func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
- _spec := &sqlgraph.UpdateSpec{
- Node: &sqlgraph.NodeSpec{
- Table: user.Table,
- Columns: user.Columns,
- ID: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: user.FieldID,
- },
- },
+func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
+ if err := _u.check(); err != nil {
+ return _node, err
}
- id, ok := uuo.mutation.ID()
+ _spec := sqlgraph.NewUpdateSpec(user.Table, user.Columns, sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt))
+ id, ok := _u.mutation.ID()
if !ok {
- return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing User.ID for update")}
+ return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "User.id" for update`)}
}
_spec.Node.ID.Value = id
- if fields := uuo.fields; len(fields) > 0 {
+ if fields := _u.fields; len(fields) > 0 {
_spec.Node.Columns = make([]string, 0, len(fields))
_spec.Node.Columns = append(_spec.Node.Columns, user.FieldID)
for _, f := range fields {
@@ -452,35 +471,29 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error)
}
}
}
- if ps := uuo.mutation.predicates; len(ps) > 0 {
+ if ps := _u.mutation.predicates; len(ps) > 0 {
_spec.Predicate = func(selector *sql.Selector) {
for i := range ps {
ps[i](selector)
}
}
}
- if value, ok := uuo.mutation.Name(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: user.FieldName,
- })
+ if value, ok := _u.mutation.Name(); ok {
+ _spec.SetField(user.FieldName, field.TypeString, value)
}
- if value, ok := uuo.mutation.Email(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: user.FieldEmail,
- })
+ if value, ok := _u.mutation.Email(); ok {
+ _spec.SetField(user.FieldEmail, field.TypeString, value)
}
- if value, ok := uuo.mutation.Password(); ok {
- _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
- Type: field.TypeString,
- Value: value,
- Column: user.FieldPassword,
- })
+ if value, ok := _u.mutation.Password(); ok {
+ _spec.SetField(user.FieldPassword, field.TypeString, value)
}
- if uuo.mutation.OwnerCleared() {
+ if value, ok := _u.mutation.Verified(); ok {
+ _spec.SetField(user.FieldVerified, field.TypeBool, value)
+ }
+ if value, ok := _u.mutation.Admin(); ok {
+ _spec.SetField(user.FieldAdmin, field.TypeBool, value)
+ }
+ if _u.mutation.OwnerCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
@@ -488,15 +501,12 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error)
Columns: []string{user.OwnerColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
- if nodes := uuo.mutation.RemovedOwnerIDs(); len(nodes) > 0 && !uuo.mutation.OwnerCleared() {
+ if nodes := _u.mutation.RemovedOwnerIDs(); len(nodes) > 0 && !_u.mutation.OwnerCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
@@ -504,10 +514,7 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error)
Columns: []string{user.OwnerColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt),
},
}
for _, k := range nodes {
@@ -515,7 +522,7 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
- if nodes := uuo.mutation.OwnerIDs(); len(nodes) > 0 {
+ if nodes := _u.mutation.OwnerIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: true,
@@ -523,10 +530,7 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error)
Columns: []string{user.OwnerColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
- IDSpec: &sqlgraph.FieldSpec{
- Type: field.TypeInt,
- Column: passwordtoken.FieldID,
- },
+ IDSpec: sqlgraph.NewFieldSpec(passwordtoken.FieldID, field.TypeInt),
},
}
for _, k := range nodes {
@@ -534,16 +538,17 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
- _node = &User{config: uuo.config}
+ _node = &User{config: _u.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues
- if err = sqlgraph.UpdateNode(ctx, uuo.driver, _spec); err != nil {
+ if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{user.Label}
} else if sqlgraph.IsConstraintError(err) {
- err = &ConstraintError{err.Error(), err}
+ err = &ConstraintError{msg: err.Error(), wrap: err}
}
return nil, err
}
+ _u.mutation.done = true
return _node, nil
}
diff --git a/funcmap/funcmap.go b/funcmap/funcmap.go
deleted file mode 100644
index e847b11..0000000
--- a/funcmap/funcmap.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package funcmap
-
-import (
- "fmt"
- "html/template"
- "reflect"
- "strings"
-
- "goweb/config"
-
- "github.com/Masterminds/sprig"
- "github.com/labstack/gommon/random"
-)
-
-var (
- // CacheBuster stores a random string used as a cache buster for static files.
- CacheBuster = random.String(10)
-)
-
-// GetFuncMap provides a template function map
-func GetFuncMap() template.FuncMap {
- // See http://masterminds.github.io/sprig/ for available funcs
- funcMap := sprig.FuncMap()
-
- // Provide a list of custom functions
- // Expand this as you add more functions to this package
- // Avoid using a name already in use by sprig
- f := template.FuncMap{
- "hasField": HasField,
- "file": File,
- "link": Link,
- }
-
- for k, v := range f {
- funcMap[k] = v
- }
-
- return funcMap
-}
-
-// HasField checks if an interface contains a given field
-func HasField(v interface{}, name string) bool {
- rv := reflect.ValueOf(v)
- if rv.Kind() == reflect.Ptr {
- rv = rv.Elem()
- }
- if rv.Kind() != reflect.Struct {
- return false
- }
- return rv.FieldByName(name).IsValid()
-}
-
-// File appends a cache buster to a given filepath so it can remain cached until the app is restarted
-func File(filepath string) string {
- return fmt.Sprintf("/%s/%s?v=%s", config.StaticPrefix, filepath, CacheBuster)
-}
-
-// Link outputs HTML for a link element, providing the ability to dynamically set the active class
-func Link(url, text, currentPath string, classes ...string) template.HTML {
- if currentPath == url {
- classes = append(classes, "is-active")
- }
-
- html := fmt.Sprintf(`%s`, strings.Join(classes, " "), url, text)
- return template.HTML(html)
-}
diff --git a/funcmap/funcmap_test.go b/funcmap/funcmap_test.go
deleted file mode 100644
index 8c27005..0000000
--- a/funcmap/funcmap_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package funcmap
-
-import (
- "fmt"
- "testing"
-
- "goweb/config"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestHasField(t *testing.T) {
- type example struct {
- name string
- }
- var e example
- assert.True(t, HasField(e, "name"))
- assert.False(t, HasField(e, "abcd"))
-}
-
-func TestLink(t *testing.T) {
- link := string(Link("/abc", "Text", "/abc"))
- expected := `Text`
- assert.Equal(t, expected, link)
-
- link = string(Link("/abc", "Text", "/abc", "first", "second"))
- expected = `Text`
- assert.Equal(t, expected, link)
-
- link = string(Link("/abc", "Text", "/def"))
- expected = `Text`
- assert.Equal(t, expected, link)
-}
-
-func TestGetFuncMap(t *testing.T) {
- file := File("test.png")
- expected := fmt.Sprintf("/%s/test.png?v=%s", config.StaticPrefix, CacheBuster)
- assert.Equal(t, expected, file)
-}
diff --git a/go.mod b/go.mod
index c2e1442..c0f27aa 100644
--- a/go.mod
+++ b/go.mod
@@ -1,78 +1,68 @@
-module goweb
+module github.com/mikestefanello/pagoda
-go 1.17
+go 1.24.6
require (
- entgo.io/ent v0.9.1
- github.com/Masterminds/sprig v2.22.0+incompatible
- github.com/PuerkitoBio/goquery v1.8.0
- github.com/eko/gocache/v2 v2.1.0
- github.com/go-playground/assert/v2 v2.0.1
- github.com/go-playground/validator/v10 v10.9.0
- github.com/go-redis/redis/v8 v8.11.4
- github.com/gorilla/sessions v1.2.1
- github.com/jackc/pgx/v4 v4.14.1
- github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
- github.com/labstack/echo-contrib v0.11.0
- github.com/labstack/echo/v4 v4.6.1
- github.com/labstack/gommon v0.3.1
- github.com/stretchr/testify v1.7.0
- golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
+ entgo.io/ent v0.14.5
+ github.com/PuerkitoBio/goquery v1.10.3
+ github.com/go-playground/validator/v10 v10.29.0
+ github.com/golang-jwt/jwt/v5 v5.3.0
+ github.com/gorilla/context v1.1.2
+ github.com/gorilla/sessions v1.4.0
+ github.com/labstack/echo/v4 v4.14.0
+ github.com/mattn/go-sqlite3 v1.14.32
+ github.com/maypok86/otter v1.2.4
+ github.com/mikestefanello/backlite v0.6.0
+ github.com/spf13/afero v1.15.0
+ github.com/spf13/viper v1.21.0
+ github.com/stretchr/testify v1.11.1
+ golang.org/x/crypto v0.46.0
+ maragu.dev/gomponents v1.2.0
)
require (
- github.com/Masterminds/goutils v1.1.1 // indirect
- github.com/Masterminds/semver v1.5.0 // indirect
- github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
- github.com/andybalholm/cascadia v1.3.1 // indirect
- github.com/beorn7/perks v1.0.1 // indirect
- github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
- github.com/cenkalti/backoff/v4 v4.1.0 // indirect
- github.com/cespare/xxhash/v2 v2.1.2 // indirect
- github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
- github.com/go-playground/locales v0.14.0 // indirect
- github.com/go-playground/universal-translator v0.18.0 // indirect
- github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
- github.com/golang/protobuf v1.5.2 // indirect
- github.com/google/uuid v1.3.0 // indirect
- github.com/gorilla/context v1.1.1 // indirect
- github.com/gorilla/securecookie v1.1.1 // indirect
- github.com/huandu/xstrings v1.3.2 // indirect
- github.com/imdario/mergo v0.3.12 // indirect
- github.com/jackc/chunkreader/v2 v2.0.1 // indirect
- github.com/jackc/pgconn v1.10.1 // indirect
- github.com/jackc/pgio v1.0.0 // indirect
- github.com/jackc/pgpassfile v1.0.0 // indirect
- github.com/jackc/pgproto3/v2 v2.2.0 // indirect
- github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
- github.com/jackc/pgtype v1.9.1 // indirect
- github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
- github.com/leodido/go-urn v1.2.1 // indirect
- github.com/mattn/go-colorable v0.1.12 // indirect
- github.com/mattn/go-isatty v0.0.14 // indirect
- github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
- github.com/mitchellh/copystructure v1.2.0 // indirect
- github.com/mitchellh/reflectwalk v1.0.2 // indirect
- github.com/pegasus-kv/thrift v0.13.0 // indirect
- github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/client_golang v1.10.0 // indirect
- github.com/prometheus/client_model v0.2.0 // indirect
- github.com/prometheus/common v0.25.0 // indirect
- github.com/prometheus/procfs v0.6.0 // indirect
- github.com/sirupsen/logrus v1.6.0 // indirect
- github.com/spf13/cast v1.3.1 // indirect
+ ariga.io/atlas v0.38.0 // indirect
+ github.com/agext/levenshtein v1.2.3 // indirect
+ github.com/andybalholm/cascadia v1.3.3 // indirect
+ github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+ github.com/bmatcuk/doublestar v1.3.4 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dolthub/maphash v0.1.0 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.12 // indirect
+ github.com/gammazero/deque v1.2.0 // indirect
+ github.com/go-openapi/inflect v0.21.5 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/securecookie v1.1.2 // indirect
+ github.com/hashicorp/hcl/v2 v2.24.0 // indirect
+ github.com/labstack/gommon v0.4.2 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/rogpeppe/go-internal v1.10.0 // indirect
+ github.com/sagikazarmark/locafero v0.12.0 // indirect
+ github.com/spf13/cast v1.10.0 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fasttemplate v1.2.1 // indirect
- github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
- golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 // indirect
- golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect
- golang.org/x/text v0.3.7 // indirect
- golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
- google.golang.org/appengine v1.6.1 // indirect
- google.golang.org/protobuf v1.26.0 // indirect
- gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
- gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
- gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
- k8s.io/apimachinery v0.0.0-20191123233150-4c4803ed55e3 // indirect
+ github.com/valyala/fasttemplate v1.2.2 // indirect
+ github.com/zclconf/go-cty v1.17.0 // indirect
+ github.com/zclconf/go-cty-yaml v1.1.0 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/mod v0.31.0 // indirect
+ golang.org/x/net v0.48.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
+ golang.org/x/time v0.14.0 // indirect
+ golang.org/x/tools v0.40.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index b1b4126..134f800 100644
--- a/go.sum
+++ b/go.sum
@@ -1,932 +1,204 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-entgo.io/ent v0.9.1 h1:IG8andyeD79GG24U8Q+1Y45hQXj6gY5evSBcva5gtBk=
-entgo.io/ent v0.9.1/go.mod h1:6NUeTfUN5mp5YN+5tgoH1SlakSvYPTBOYotSOvaI4ak=
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+ariga.io/atlas v0.38.0 h1:MwbtwVtDWJFq+ECyeTAz2ArvewDnpeiw/t/sgNdDsdo=
+ariga.io/atlas v0.38.0/go.mod h1:D7XMK6ei3GvfDqvzk+2VId78j77LdqHrqPOWamn51/s=
+entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
+entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
-github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
-github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
-github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
-github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
-github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
-github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
-github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
-github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
-github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
-github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
-github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
-github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
-github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
-github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
-github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
-github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
-github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 h1:pami0oPhVosjOu/qRHepRmdjD6hGILF7DBr+qQZeP10=
-github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2/go.mod h1:jNIx5ykW1MroBuaTja9+VpglmaJOUzezumfhLlER3oY=
-github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
-github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/allegro/bigcache/v2 v2.2.5 h1:mRc8r6GQjuJsmSKQNPsR5jQVXc8IJ1xsW5YXUYMLfqI=
-github.com/allegro/bigcache/v2 v2.2.5/go.mod h1:FppZsIO+IZk7gCuj5FiIDHGygD9xvWQcqg1uIPMb6tY=
-github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
-github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
-github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
-github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
-github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
-github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
-github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
-github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
-github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
-github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
-github.com/casbin/casbin/v2 v2.31.2/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
-github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
-github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
-github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
-github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
-github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
-github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
-github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
-github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
-github.com/coocood/freecache v1.1.1 h1:uukNF7QKCZEdZ9gAV7WQzvh0SbjwdMF6m3x3rxEkaPc=
-github.com/coocood/freecache v1.1.1/go.mod h1:OKrEjkGVoxZhyWAJoeFi5BMLUJm2Tit0kpGkIr7NGYY=
-github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI=
-github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
-github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
-github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
-github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
-github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
-github.com/eko/gocache/v2 v2.1.0 h1:ljFKAAa5hHsrsSaBvyx0g9a/A9lZSUrf4jBjErQd7gc=
-github.com/eko/gocache/v2 v2.1.0/go.mod h1:u+EpYjCVsOpeqvDLzinOVLjLxwHJjO+NT4LS2+8XnCU=
-github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
-github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
-github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
-github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
-github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
-github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
-github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-bindata/go-bindata v1.0.1-0.20190711162640-ee3c2418e368/go.mod h1:7xCgX1lzlrXPHkfvn3EhumqHkmSlzt8at9q7v0ax19c=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
-github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
-github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
-github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
-github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
-github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
-github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
-github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
-github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
-github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
-github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
-github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
-github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
-github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A=
-github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
-github.com/go-redis/redis/v8 v8.9.0/go.mod h1:ik7vb7+gm8Izylxu6kf6wG26/t2VljgCfSQ1DM4O1uU=
-github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
-github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
-github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
-github.com/go-sql-driver/mysql v1.5.1-0.20200311113236-681ffa848bae/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
-github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
-github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
-github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
-github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
-github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
-github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
-github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
-github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
-github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
-github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
-github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
-github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
-github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
-github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
-github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
-github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
-github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
-github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
-github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
-github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
-github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
-github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
-github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
-github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
-github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
-github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
-github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8=
-github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
-github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
-github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
-github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
-github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
-github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
-github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
-github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
-github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
-github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
-github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
-github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
-github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
-github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
-github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
-github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
-github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
-github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
-github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
-github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
-github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
-github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
-github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
-github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
-github.com/jackc/pgtype v1.9.1 h1:MJc2s0MFS8C3ok1wQTdQxWuXQcB6+HwAm5x1CzW7mf0=
-github.com/jackc/pgtype v1.9.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
-github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
-github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
-github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
-github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
-github.com/jackc/pgx/v4 v4.14.1 h1:71oo1KAGI6mXhLiTMn6iDFcp3e7+zon/capWjl2OEFU=
-github.com/jackc/pgx/v4 v4.14.1/go.mod h1:RgDuE4Z34o7XE92RpLsvFiOEfrAUT0Xt2KxvX73W06M=
-github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
-github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
-github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
-github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
-github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
-github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd h1:nIzoSW6OhhppWLm4yqBwZsKJlAayUu5FGozhrF3ETSM=
-github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd/go.mod h1:MEQrHur0g8VplbLOv5vXmDzacSaH9Z7XhcgsSh1xciU=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
-github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
+github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
+github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
+github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
+github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
+github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
+github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
+github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
+github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
+github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
+github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
+github.com/gammazero/deque v1.2.0 h1:scEFO8Uidhw6KDU5qg1HA5fYwM0+us2qdeJqm43bitU=
+github.com/gammazero/deque v1.2.0/go.mod h1:JVrR+Bj1NMQbPnYclvDlvSX0nVGReLrQZ0aUMuWLctg=
+github.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA=
+github.com/go-openapi/inflect v0.21.5/go.mod h1:GypUyi6bU880NYurWaEH2CmH84zFDNd+EhhmzroHmB4=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
+github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
+github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
+github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
+github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
+github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
+github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
+github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
+github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/labstack/echo-contrib v0.11.0 h1:/B7meUKBP7AAoSEOrawpSivhFvu7GQG+kDhlzi5v0Wo=
-github.com/labstack/echo-contrib v0.11.0/go.mod h1:Hk8Iyxe2GrYR/ch0cbI3BK7ZhR2Y60YEqtkoZilqDOc=
-github.com/labstack/echo/v4 v4.3.0/go.mod h1:PvmtTvhVqKDzDQy4d3bWzPjZLzom4iQbAZy2sgZ/qI8=
-github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M=
-github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
-github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
-github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
-github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
-github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
-github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
-github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
-github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
-github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
-github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
-github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
-github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
-github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
-github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
-github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
-github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
-github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
-github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
-github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
-github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
-github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
-github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
-github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
-github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
-github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
-github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
-github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
-github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
-github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
-github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
-github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
-github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
-github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
-github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
-github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
-github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
-github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
-github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
-github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
-github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
-github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
-github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
-github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
-github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
-github.com/pegasus-kv/thrift v0.13.0 h1:4ESwaNoHImfbHa9RUGJiJZ4hrxorihZHk5aarYwY8d4=
-github.com/pegasus-kv/thrift v0.13.0/go.mod h1:Gl9NT/WHG6ABm6NsrbfE8LiJN0sAyneCrvB4qN4NPqQ=
-github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
-github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
-github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
-github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
-github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.10.0 h1:/o0BDeWzLWXNZ+4q5gXltUvaMpJqckTa+jTNoB+z4cg=
-github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
-github.com/prometheus/common v0.25.0 h1:IjJYZJCI8HZYtqA3xYwGyDzSCy1r4CA2GRh+4vdOmtE=
-github.com/prometheus/common v0.25.0/go.mod h1:H6QK/N6XVT42whUeIdI3dp36w49c+/iMDk7UAI2qm7Q=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
-github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
-github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
-github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
-github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
-github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
-github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
-github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
-github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
-github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
-github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
-github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
-github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
-github.com/uber/jaeger-lib v2.4.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
-github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
-github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/labstack/echo/v4 v4.14.0 h1:+tiMrDLxwv6u0oKtD03mv+V1vXXB3wCqPHJqPuIe+7M=
+github.com/labstack/echo/v4 v4.14.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
+github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
+github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
+github.com/mikestefanello/backlite v0.6.0 h1:kpQKxR5NGHWvtAZuR0AsEZo95g967FY9fnU9y7YYvAY=
+github.com/mikestefanello/backlite v0.6.0/go.mod h1:gx6UKLUQY5OVXQkIm3AzNkyPn9OzoKHKuwM4JGrY4tQ=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
+github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
+github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
+github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
+github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
-github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
-github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
-github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
-github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
-go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
-go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
-go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
-go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
-go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
-go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
-go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
-go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
-go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
-go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
-go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
+github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
+github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
+github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
+github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
+github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
-golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
+golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
-golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 h1:0qxwC5n+ttVOINCBeRHO0nq9X7uy8SDsPoi5OaCdIEI=
-golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
-golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
-golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
+golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
-gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
-gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
-gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
-google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
-gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
-gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
-gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
-gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
-gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-k8s.io/apimachinery v0.0.0-20191123233150-4c4803ed55e3 h1:FErmbNIJruD5GT2oVEjtPn5Ar5+rcWJsC8/PPUkR0s4=
-k8s.io/apimachinery v0.0.0-20191123233150-4c4803ed55e3/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
-k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
-k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
-k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
-k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
-sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
-sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
-sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+maragu.dev/gomponents v1.2.0 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc=
+maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
diff --git a/htmx/htmx.go b/htmx/htmx.go
deleted file mode 100644
index b14c76d..0000000
--- a/htmx/htmx.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package htmx
-
-import (
- "net/http"
-
- "github.com/labstack/echo/v4"
-)
-
-// Headers (https://htmx.org/docs/#requests)
-const (
- HeaderRequest = "HX-Request"
- HeaderBoosted = "HX-Boosted"
- HeaderTrigger = "HX-Trigger"
- HeaderTriggerName = "HX-Trigger-Name"
- HeaderTriggerAfterSwap = "HX-Trigger-After-Swap"
- HeaderTriggerAfterSettle = "HX-Trigger-After-Settle"
- HeaderTarget = "HX-Target"
- HeaderPrompt = "HX-Prompt"
- HeaderPush = "HX-Push"
- HeaderRedirect = "HX-Redirect"
- HeaderRefresh = "HX-Refresh"
-)
-
-type (
- // Request contains data that HTMX provides during requests
- Request struct {
- Enabled bool
- Boosted bool
- Trigger string
- TriggerName string
- Target string
- Prompt string
- }
-
- // Response contain data that the server can communicate back to HTMX
- Response struct {
- Push string
- Redirect string
- Refresh bool
- Trigger string
- TriggerAfterSwap string
- TriggerAfterSettle string
- NoContent bool
- }
-)
-
-// GetRequest extracts HTMX data from the request
-func GetRequest(ctx echo.Context) Request {
- return Request{
- Enabled: ctx.Request().Header.Get(HeaderRequest) == "true",
- Boosted: ctx.Request().Header.Get(HeaderBoosted) == "true",
- Trigger: ctx.Request().Header.Get(HeaderTrigger),
- TriggerName: ctx.Request().Header.Get(HeaderTriggerName),
- Target: ctx.Request().Header.Get(HeaderTarget),
- Prompt: ctx.Request().Header.Get(HeaderPrompt),
- }
-}
-
-// Apply applies data from a Response to a server response
-func (r Response) Apply(ctx echo.Context) {
- if r.Push != "" {
- ctx.Response().Header().Set(HeaderPush, r.Push)
- }
- if r.Redirect != "" {
- ctx.Response().Header().Set(HeaderRedirect, r.Redirect)
- }
- if r.Refresh {
- ctx.Response().Header().Set(HeaderRefresh, "true")
- }
- if r.Trigger != "" {
- ctx.Response().Header().Set(HeaderTrigger, r.Trigger)
- }
- if r.TriggerAfterSwap != "" {
- ctx.Response().Header().Set(HeaderTriggerAfterSwap, r.TriggerAfterSwap)
- }
- if r.TriggerAfterSettle != "" {
- ctx.Response().Header().Set(HeaderTriggerAfterSettle, r.TriggerAfterSettle)
- }
- if r.NoContent {
- ctx.Response().Status = http.StatusNoContent
- }
-}
diff --git a/main.go b/main.go
deleted file mode 100644
index c8bb37f..0000000
--- a/main.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package main
-
-import (
- "context"
- "crypto/tls"
- "fmt"
- "net/http"
- "os"
- "os/signal"
- "time"
-
- "goweb/routes"
- "goweb/services"
-)
-
-func main() {
- // Start a new container
- c := services.NewContainer()
- defer func() {
- if err := c.Shutdown(); err != nil {
- c.Web.Logger.Fatal(err)
- }
- }()
-
- // Build the router
- routes.BuildRouter(c)
-
- // 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)
- if err != nil {
- c.Web.Logger.Fatalf("cannot load TLS certificate: %v", err)
- }
-
- srv.TLSConfig = &tls.Config{
- Certificates: []tls.Certificate{certs},
- }
- }
-
- if err := c.Web.StartServer(&srv); err != http.ErrServerClosed {
- c.Web.Logger.Fatalf("shutting down the server: v", err)
- }
- }()
-
- // Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds.
- quit := make(chan os.Signal, 1)
- signal.Notify(quit, os.Interrupt)
- signal.Notify(quit, os.Kill)
- <-quit
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- if err := c.Web.Shutdown(ctx); err != nil {
- c.Web.Logger.Fatal(err)
- }
-}
diff --git a/middleware/auth.go b/middleware/auth.go
deleted file mode 100644
index 8b20037..0000000
--- a/middleware/auth.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package middleware
-
-import (
- "net/http"
-
- "goweb/context"
- "goweb/ent"
- "goweb/msg"
- "goweb/services"
-
- "github.com/labstack/echo/v4"
-)
-
-// LoadAuthenticatedUser loads the authenticated user, if one, and stores in context
-func LoadAuthenticatedUser(authClient *services.AuthClient) echo.MiddlewareFunc {
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- u, err := authClient.GetAuthenticatedUser(c)
- switch err.(type) {
- case *ent.NotFoundError:
- c.Logger().Warn("auth user not found")
- case services.NotAuthenticatedError:
- case nil:
- c.Set(context.AuthenticatedUserKey, u)
- c.Logger().Infof("auth user loaded in to context: %d", u.ID)
- default:
- c.Logger().Errorf("error querying for authenticated user: %v", err)
- }
-
- return next(c)
- }
- }
-}
-
-// LoadValidPasswordToken loads a valid password token entity that matches the user and token
-// provided in path parameters
-// If the token is invalid, the user will be redirected to the forgot password route
-// This requires that the user owning the token is loaded in to context
-func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc {
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- // Extract the user parameter
- if c.Get(context.UserKey) == nil {
- return echo.NewHTTPError(http.StatusInternalServerError)
- }
- usr := c.Get(context.UserKey).(*ent.User)
-
- token, err := authClient.GetValidPasswordToken(c, c.Param("password_token"), usr.ID)
-
- switch err.(type) {
- case nil:
- c.Set(context.PasswordTokenKey, token)
- return next(c)
- case services.InvalidPasswordTokenError:
- msg.Warning(c, "The link is either invalid or has expired. Please request a new one.")
- return c.Redirect(http.StatusFound, c.Echo().Reverse("forgot_password"))
- default:
- c.Logger().Error(err)
- return echo.NewHTTPError(http.StatusInternalServerError)
- }
- }
- }
-}
-
-// RequireAuthentication requires that the user be authenticated in order to proceed
-func RequireAuthentication() echo.MiddlewareFunc {
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- if u := c.Get(context.AuthenticatedUserKey); u == nil {
- return echo.NewHTTPError(http.StatusUnauthorized)
- }
-
- return next(c)
- }
- }
-}
-
-// RequireNoAuthentication requires that the user not be authenticated in order to proceed
-func RequireNoAuthentication() echo.MiddlewareFunc {
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- if u := c.Get(context.AuthenticatedUserKey); u != nil {
- return echo.NewHTTPError(http.StatusForbidden)
- }
-
- return next(c)
- }
- }
-}
diff --git a/middleware/cache.go b/middleware/cache.go
deleted file mode 100644
index ca59fbf..0000000
--- a/middleware/cache.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package middleware
-
-import (
- "fmt"
- "net/http"
- "time"
-
- "goweb/context"
-
- "github.com/eko/gocache/v2/cache"
- "github.com/eko/gocache/v2/marshaler"
- "github.com/go-redis/redis/v8"
- "github.com/labstack/echo/v4"
-)
-
-// CachedPage is what is used to store a rendered Page in the cache
-type CachedPage struct {
- // URL stores the URL of the requested page
- URL string
-
- // HTML stores the complete HTML of the rendered Page
- HTML []byte
-
- // StatusCode stores the HTTP status code
- StatusCode int
-
- // Headers stores the HTTP headers
- Headers map[string]string
-}
-
-// ServeCachedPage attempts to load a page from the cache by matching on the complete request URL
-// If a page is cached for the requested URL, it will be served here and the request terminated.
-// Any request made by an authenticated user or that is not a GET will be skipped.
-func ServeCachedPage(ch *cache.Cache) echo.MiddlewareFunc {
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- // Skip non GET requests
- if c.Request().Method != http.MethodGet {
- return next(c)
- }
-
- // Skip if the user is authenticated
- if c.Get(context.AuthenticatedUserKey) != nil {
- return next(c)
- }
-
- // Attempt to load from cache
- res, err := marshaler.New(ch).Get(
- c.Request().Context(),
- c.Request().URL.String(),
- new(CachedPage),
- )
- if err != nil {
- if err == redis.Nil {
- c.Logger().Info("no cached page found")
- } else {
- c.Logger().Errorf("failed getting cached page: %v", err)
- }
- return next(c)
- }
-
- page, ok := res.(*CachedPage)
- if !ok {
- c.Logger().Errorf("failed casting cached page")
- return next(c)
- }
-
- // Set any headers
- if page.Headers != nil {
- for k, v := range page.Headers {
- c.Response().Header().Set(k, v)
- }
- }
-
- c.Logger().Info("serving cached page")
-
- return c.HTMLBlob(page.StatusCode, page.HTML)
- }
- }
-}
-
-// CacheControl sets a Cache-Control header with a given max age
-func CacheControl(maxAge time.Duration) echo.MiddlewareFunc {
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- v := "no-cache, no-store"
- if maxAge > 0 {
- v = fmt.Sprintf("public, max-age=%.0f", maxAge.Seconds())
- }
- c.Response().Header().Set("Cache-Control", v)
- return next(c)
- }
- }
-}
diff --git a/middleware/cache_test.go b/middleware/cache_test.go
deleted file mode 100644
index 8fb447d..0000000
--- a/middleware/cache_test.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package middleware
-
-import (
- "context"
- "net/http"
- "testing"
- "time"
-
- "goweb/tests"
-
- "github.com/stretchr/testify/require"
-
- "github.com/eko/gocache/v2/marshaler"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestServeCachedPage(t *testing.T) {
- // Cache a page
- cp := CachedPage{
- URL: "/cache",
- HTML: []byte("html"),
- Headers: make(map[string]string),
- StatusCode: http.StatusCreated,
- }
- cp.Headers["a"] = "b"
- cp.Headers["c"] = "d"
- err := marshaler.New(c.Cache).Set(context.Background(), cp.URL, cp, nil)
- require.NoError(t, err)
-
- // Request the URL of the cached page
- ctx, rec := tests.NewContext(c.Web, cp.URL)
- err = tests.ExecuteMiddleware(ctx, ServeCachedPage(c.Cache))
- assert.NoError(t, err)
- assert.Equal(t, cp.StatusCode, ctx.Response().Status)
- assert.Equal(t, cp.Headers["a"], ctx.Response().Header().Get("a"))
- assert.Equal(t, cp.Headers["c"], ctx.Response().Header().Get("c"))
- assert.Equal(t, cp.HTML, rec.Body.Bytes())
-
- // Login and try again
- tests.InitSession(ctx)
- err = c.Auth.Login(ctx, usr.ID)
- require.NoError(t, err)
- _ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth))
- err = tests.ExecuteMiddleware(ctx, ServeCachedPage(c.Cache))
- assert.Nil(t, err)
-}
-
-func TestCacheControl(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/")
- _ = tests.ExecuteMiddleware(ctx, CacheControl(time.Second*5))
- assert.Equal(t, "public, max-age=5", ctx.Response().Header().Get("Cache-Control"))
- _ = tests.ExecuteMiddleware(ctx, CacheControl(0))
- assert.Equal(t, "no-cache, no-store", ctx.Response().Header().Get("Cache-Control"))
-}
diff --git a/middleware/log.go b/middleware/log.go
deleted file mode 100644
index 2c36431..0000000
--- a/middleware/log.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package middleware
-
-import (
- "fmt"
-
- "github.com/labstack/echo/v4"
-)
-
-// LogRequestID includes the request ID in all logs for the given request
-// This requires that middleware that includes the request ID first execute
-func LogRequestID() echo.MiddlewareFunc {
- return func(next echo.HandlerFunc) echo.HandlerFunc {
- return func(c echo.Context) error {
- rID := c.Response().Header().Get(echo.HeaderXRequestID)
- format := `{"time":"${time_rfc3339_nano}","id":"%s","level":"${level}","prefix":"${prefix}","file":"${short_file}","line":"${line}"}`
- c.Logger().SetHeader(fmt.Sprintf(format, rID))
- return next(c)
- }
- }
-}
diff --git a/middleware/log_test.go b/middleware/log_test.go
deleted file mode 100644
index 270fc5e..0000000
--- a/middleware/log_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package middleware
-
-import (
- "bytes"
- "fmt"
- "testing"
-
- "goweb/tests"
-
- "github.com/labstack/echo/v4"
-
- "github.com/stretchr/testify/assert"
-
- echomw "github.com/labstack/echo/v4/middleware"
-)
-
-func TestLogRequestID(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/")
- _ = tests.ExecuteMiddleware(ctx, echomw.RequestID())
- _ = tests.ExecuteMiddleware(ctx, LogRequestID())
-
- var buf bytes.Buffer
- ctx.Logger().SetOutput(&buf)
- ctx.Logger().Info("test")
- rID := ctx.Response().Header().Get(echo.HeaderXRequestID)
- assert.Contains(t, buf.String(), fmt.Sprintf(`id":"%s"`, rID))
-}
diff --git a/pkg/context/context.go b/pkg/context/context.go
new file mode 100644
index 0000000..05ad549
--- /dev/null
+++ b/pkg/context/context.go
@@ -0,0 +1,62 @@
+package context
+
+import (
+ "context"
+ "errors"
+
+ "github.com/labstack/echo/v4"
+)
+
+const (
+ // AuthenticatedUserKey is the key used to store the authenticated user in context.
+ AuthenticatedUserKey = "auth_user"
+
+ // UserKey is the key used to store a user in context.
+ UserKey = "user"
+
+ // FormKey is the key used to store a form in context.
+ FormKey = "form"
+
+ // PasswordTokenKey is the key used to store a password token in context.
+ PasswordTokenKey = "password_token"
+
+ // LoggerKey is the key used to store a structured logger in context.
+ LoggerKey = "logger"
+
+ // SessionKey is the key used to store the session data in context.
+ SessionKey = "session"
+
+ // HTMXRequestKey is the key used to store the HTMX request data in context.
+ HTMXRequestKey = "htmx"
+
+ // CSRFKey is the key used to store the CSRF token in context.
+ CSRFKey = "csrf"
+
+ // ConfigKey is the key used to store the configuration in context.
+ ConfigKey = "config"
+
+ // AdminEntityKey is the key used to store the entity being operated on in the admin panel.
+ AdminEntityKey = "admin:entity"
+
+ // AdminEntityIDKey is the key used to store the ID of the entity being operated on in the admin panel.
+ AdminEntityIDKey = "admin:entity_id"
+)
+
+// IsCanceledError determines if an error is due to a context cancellation.
+func IsCanceledError(err error) bool {
+ return errors.Is(err, context.Canceled)
+}
+
+// Cache checks if a value of a given type exists in the Echo context for a given key and returns that, otherwise
+// it will use a callback to generate a value, which is stored in the context then returned. This allows you to
+// only generate items only once for a given request.
+func Cache[T any](ctx echo.Context, key string, gen func(echo.Context) T) T {
+ if val := ctx.Get(key); val != nil {
+ if v, ok := val.(T); ok {
+ return v
+ }
+ }
+ val := gen(ctx)
+ ctx.Set(key, val)
+ return val
+}
diff --git a/pkg/context/context_test.go b/pkg/context/context_test.go
new file mode 100644
index 0000000..536716a
--- /dev/null
+++ b/pkg/context/context_test.go
@@ -0,0 +1,47 @@
+package context
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIsCanceled(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ assert.False(t, IsCanceledError(ctx.Err()))
+ cancel()
+ assert.True(t, IsCanceledError(ctx.Err()))
+
+ ctx, cancel = context.WithTimeout(context.Background(), time.Microsecond*5)
+ <-ctx.Done()
+ cancel()
+ assert.False(t, IsCanceledError(ctx.Err()))
+
+ assert.False(t, IsCanceledError(errors.New("test error")))
+}
+
+func TestCache(t *testing.T) {
+ ctx := echo.New().NewContext(nil, nil)
+
+ key := "testing"
+ value := "hello"
+ called := 0
+ callback := func(ctx echo.Context) string {
+ called++
+ return value
+ }
+
+ assert.Nil(t, ctx.Get(key))
+
+ got := Cache(ctx, key, callback)
+ assert.Equal(t, value, got)
+ assert.Equal(t, 1, called)
+
+ got = Cache(ctx, key, callback)
+ assert.Equal(t, value, got)
+ assert.Equal(t, 1, called)
+}
diff --git a/pkg/form/form.go b/pkg/form/form.go
new file mode 100644
index 0000000..bdd5ada
--- /dev/null
+++ b/pkg/form/form.go
@@ -0,0 +1,55 @@
+package form
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/context"
+)
+
+// Form represents a form that can be submitted and validated.
+type Form interface {
+ // Submit marks the form as submitted, stores a pointer to it in the context, binds the request
+ // values to the struct fields, and validates the input based on the struct tags.
+ // Returns a validator.ValidationErrors, if the form values were not valid, or an echo.HTTPError,
+ // if the request failed to process.
+ Submit(c echo.Context, form any) error
+
+ // IsSubmitted returns true if the form was submitted.
+ IsSubmitted() bool
+
+ // IsValid returns true if the form has no validation errors.
+ IsValid() bool
+
+ // IsDone returns true if the form was submitted and has no validation errors.
+ IsDone() bool
+
+ // FieldHasErrors returns true if a given struct field has validation errors.
+ FieldHasErrors(fieldName string) bool
+
+ // SetFieldError sets a validation error message for a given struct field.
+ SetFieldError(fieldName string, message string)
+
+ // GetFieldErrors returns the validation errors for a given struct field.
+ GetFieldErrors(fieldName string) []string
+}
+
+// Get gets a form from the context or initializes a new copy if one is not set.
+func Get[T any](ctx echo.Context) *T {
+ if v := ctx.Get(context.FormKey); v != nil {
+ if form, ok := v.(*T); ok {
+ return form
+ }
+ }
+ var v T
+ return &v
+}
+
+// Clear removes the form set in the context.
+func Clear(ctx echo.Context) {
+ ctx.Set(context.FormKey, nil)
+}
+
+// Submit submits a form.
+// See Form.Submit().
+func Submit(ctx echo.Context, form Form) error {
+ return form.Submit(ctx, form)
+}
diff --git a/pkg/form/form_test.go b/pkg/form/form_test.go
new file mode 100644
index 0000000..cbd957d
--- /dev/null
+++ b/pkg/form/form_test.go
@@ -0,0 +1,67 @@
+package form
+
+import (
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/tests"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type mockForm struct {
+ called bool
+ Submission
+}
+
+func (m *mockForm) Submit(_ echo.Context, _ any) error {
+ m.called = true
+ return nil
+}
+
+func TestSubmit(t *testing.T) {
+ m := mockForm{}
+ ctx, _ := tests.NewContext(echo.New(), "/")
+ err := Submit(ctx, &m)
+ require.NoError(t, err)
+ assert.True(t, m.called)
+}
+
+func TestGetClear(t *testing.T) {
+ e := echo.New()
+
+ type example struct {
+ Name string `form:"name"`
+ }
+
+ t.Run("get empty context", func(t *testing.T) {
+ // Empty context, still return a form.
+ ctx, _ := tests.NewContext(e, "/")
+ form := Get[example](ctx)
+ assert.NotNil(t, form)
+ })
+
+ t.Run("get non-empty context", func(t *testing.T) {
+ form := example{
+ Name: "test",
+ }
+ ctx, _ := tests.NewContext(e, "/")
+ ctx.Set(context.FormKey, &form)
+
+ // Get again and expect the values were stored.
+ got := Get[example](ctx)
+ require.NotNil(t, got)
+ assert.Equal(t, "test", got.Name)
+
+ // Attempt getting a different type to ensure there's no panic.
+ ret := Get[int](ctx)
+ require.NotNil(t, ret)
+
+ // Clear.
+ Clear(ctx)
+ got = Get[example](ctx)
+ require.NotNil(t, got)
+ assert.Empty(t, got.Name)
+ })
+}
diff --git a/pkg/form/submission.go b/pkg/form/submission.go
new file mode 100644
index 0000000..48fdc8a
--- /dev/null
+++ b/pkg/form/submission.go
@@ -0,0 +1,105 @@
+package form
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/mikestefanello/pagoda/pkg/context"
+
+ "github.com/labstack/echo/v4"
+)
+
+// Submission represents the state of the submission of a form, not including the form itself.
+// This satisfies the Form interface.
+type Submission 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
+}
+
+func (f *Submission) Submit(ctx echo.Context, form any) error {
+ f.isSubmitted = true
+
+ // Set in context so the form can later be retrieved.
+ ctx.Set(context.FormKey, form)
+
+ // Bind the values from the incoming request to the form struct.
+ if err := ctx.Bind(form); err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("unable to bind form: %v", err))
+ }
+
+ // Validate the form.
+ if err := ctx.Validate(form); err != nil {
+ f.setErrorMessages(err)
+ return err
+ }
+
+ return nil
+}
+
+func (f *Submission) IsSubmitted() bool {
+ return f.isSubmitted
+}
+
+func (f *Submission) IsValid() bool {
+ if f.errors == nil {
+ return true
+ }
+ return len(f.errors) == 0
+}
+
+func (f *Submission) IsDone() bool {
+ return f.IsSubmitted() && f.IsValid()
+}
+
+func (f *Submission) FieldHasErrors(fieldName string) bool {
+ return len(f.GetFieldErrors(fieldName)) > 0
+}
+
+func (f *Submission) SetFieldError(fieldName string, message string) {
+ if f.errors == nil {
+ f.errors = make(map[string][]string)
+ }
+ f.errors[fieldName] = append(f.errors[fieldName], message)
+}
+
+func (f *Submission) GetFieldErrors(fieldName string) []string {
+ if f.errors == nil {
+ return []string{}
+ }
+ return f.errors[fieldName]
+}
+
+// setErrorMessages sets errors messages on the submission for all fields that failed validation.
+func (f *Submission) 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."
+ case "gte":
+ message = fmt.Sprintf("Must be greater than or equal to %v.", ve.Param())
+ default:
+ message = "Invalid value."
+ }
+
+ // Add the error.
+ f.SetFieldError(ve.Field(), message)
+ }
+}
diff --git a/pkg/form/submission_test.go b/pkg/form/submission_test.go
new file mode 100644
index 0000000..e6b4f9b
--- /dev/null
+++ b/pkg/form/submission_test.go
@@ -0,0 +1,57 @@
+package form
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestFormSubmission(t *testing.T) {
+ type formTest struct {
+ Name string `form:"name" validate:"required"`
+ Email string `form:"email" validate:"required,email"`
+ Submission
+ }
+
+ e := echo.New()
+ e.Validator = services.NewValidator()
+
+ t.Run("valid request", func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("email=a@a.com"))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ ctx := e.NewContext(req, httptest.NewRecorder())
+
+ var form formTest
+ err := form.Submit(ctx, &form)
+ assert.IsType(t, validator.ValidationErrors{}, err)
+
+ assert.Empty(t, form.Name)
+ assert.Equal(t, "a@a.com", form.Email)
+ assert.False(t, form.IsValid())
+ assert.True(t, form.FieldHasErrors("Name"))
+ assert.False(t, form.FieldHasErrors("Email"))
+ require.Len(t, form.GetFieldErrors("Name"), 1)
+ assert.Len(t, form.GetFieldErrors("Email"), 0)
+ assert.Equal(t, "This field is required.", form.GetFieldErrors("Name")[0])
+ assert.False(t, form.IsDone())
+
+ formInCtx := Get[formTest](ctx)
+ require.NotNil(t, formInCtx)
+ assert.Equal(t, form.Email, formInCtx.Email)
+ })
+
+ t.Run("invalid request", func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("abc=abc"))
+ ctx := e.NewContext(req, httptest.NewRecorder())
+ var form formTest
+ err := form.Submit(ctx, &form)
+ assert.IsType(t, new(echo.HTTPError), err)
+ })
+}
diff --git a/pkg/handlers/admin.go b/pkg/handlers/admin.go
new file mode 100644
index 0000000..59e2504
--- /dev/null
+++ b/pkg/handlers/admin.go
@@ -0,0 +1,198 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/backlite/ui"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/ent/admin"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/middleware"
+ "github.com/mikestefanello/pagoda/pkg/msg"
+ "github.com/mikestefanello/pagoda/pkg/pager"
+ "github.com/mikestefanello/pagoda/pkg/redirect"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/pkg/ui/pages"
+)
+
+type Admin struct {
+ orm *ent.Client
+ admin *admin.Handler
+ backlite *ui.Handler
+}
+
+func init() {
+ Register(new(Admin))
+}
+
+func (h *Admin) Init(c *services.Container) error {
+ var err error
+ h.orm = c.ORM
+ h.admin = admin.NewHandler(h.orm, admin.HandlerConfig{
+ ItemsPerPage: 25,
+ PageQueryKey: pager.QueryKey,
+ TimeFormat: time.DateTime,
+ })
+ h.backlite, err = ui.NewHandler(ui.Config{
+ DB: c.Database,
+ BasePath: "/admin/tasks",
+ ItemsPerPage: 25,
+ ReleaseAfter: c.Config.Tasks.ReleaseAfter,
+ })
+ return err
+}
+
+func (h *Admin) Routes(g *echo.Group) {
+ ag := g.Group("/admin", middleware.RequireAdmin)
+
+ entities := ag.Group("/entity")
+ for _, n := range admin.GetEntityTypes() {
+ ng := entities.Group(fmt.Sprintf("/%s", strings.ToLower(n.GetName())))
+ ng.GET("", h.EntityList(n)).
+ Name = routenames.AdminEntityList(n.GetName())
+ ng.GET("/add", h.EntityAdd(n)).
+ Name = routenames.AdminEntityAdd(n.GetName())
+ ng.POST("/add", h.EntityAddSubmit(n)).
+ Name = routenames.AdminEntityAddSubmit(n.GetName())
+ ng.GET("/:id/edit", h.EntityEdit(n), h.middlewareEntityLoad(n)).
+ Name = routenames.AdminEntityEdit(n.GetName())
+ ng.POST("/:id/edit", h.EntityEditSubmit(n), h.middlewareEntityLoad(n)).
+ Name = routenames.AdminEntityEditSubmit(n.GetName())
+ ng.GET("/:id/delete", h.EntityDelete(n), h.middlewareEntityLoad(n)).
+ Name = routenames.AdminEntityDelete(n.GetName())
+ ng.POST("/:id/delete", h.EntityDeleteSubmit(n), h.middlewareEntityLoad(n)).
+ Name = routenames.AdminEntityDeleteSubmit(n.GetName())
+ }
+
+ tasks := ag.Group("/tasks")
+ tasks.GET("", h.Backlite(h.backlite.Running)).Name = routenames.AdminTasks
+ tasks.GET("/succeeded", h.Backlite(h.backlite.Succeeded))
+ tasks.GET("/failed", h.Backlite(h.backlite.Failed))
+ tasks.GET("/upcoming", h.Backlite(h.backlite.Upcoming))
+ tasks.GET("/task/:id", h.Backlite(h.backlite.Task))
+ tasks.GET("/completed/:id", h.Backlite(h.backlite.TaskCompleted))
+}
+
+// middlewareEntityLoad is middleware to extract the entity ID and attempt to load the given entity.
+func (h *Admin) middlewareEntityLoad(n admin.EntityType) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ id, err := strconv.Atoi(ctx.Param("id"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "invalid entity ID")
+ }
+
+ entity, err := h.admin.Get(ctx, n, id)
+ switch {
+ case err == nil:
+ ctx.Set(context.AdminEntityIDKey, id)
+ ctx.Set(context.AdminEntityKey, map[string][]string(entity))
+ return next(ctx)
+ case ent.IsNotFound(err):
+ return echo.NewHTTPError(http.StatusNotFound, "entity not found")
+ default:
+ return echo.NewHTTPError(http.StatusInternalServerError, err)
+ }
+ }
+ }
+}
+
+func (h *Admin) EntityList(n admin.EntityType) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ list, err := h.admin.List(ctx, n)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError, err)
+ }
+
+ return pages.AdminEntityList(ctx, n, list)
+ }
+}
+
+func (h *Admin) EntityAdd(n admin.EntityType) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ return pages.AdminEntityInput(ctx, n, nil)
+ }
+}
+
+func (h *Admin) EntityAddSubmit(n admin.EntityType) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ err := h.admin.Create(ctx, n)
+ if err != nil {
+ msg.Error(ctx, err.Error())
+ return h.EntityAdd(n)(ctx)
+ }
+
+ msg.Success(ctx, fmt.Sprintf("Successfully added %s.", n.GetName()))
+
+ return redirect.
+ New(ctx).
+ Route(routenames.AdminEntityList(n.GetName())).
+ StatusCode(http.StatusFound).
+ Go()
+ }
+}
+
+func (h *Admin) EntityEdit(n admin.EntityType) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ v := ctx.Get(context.AdminEntityKey).(map[string][]string)
+ return pages.AdminEntityInput(ctx, n, v)
+ }
+}
+
+func (h *Admin) EntityEditSubmit(n admin.EntityType) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ id := ctx.Get(context.AdminEntityIDKey).(int)
+ err := h.admin.Update(ctx, n, id)
+ if err != nil {
+ msg.Error(ctx, err.Error())
+ return h.EntityEdit(n)(ctx)
+ }
+
+ msg.Success(ctx, fmt.Sprintf("Updated %s.", n.GetName()))
+
+ return redirect.
+ New(ctx).
+ Route(routenames.AdminEntityList(n.GetName())).
+ StatusCode(http.StatusFound).
+ Go()
+ }
+}
+
+func (h *Admin) EntityDelete(n admin.EntityType) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ return pages.AdminEntityDelete(ctx, n)
+ }
+}
+
+func (h *Admin) EntityDeleteSubmit(n admin.EntityType) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ id := ctx.Get(context.AdminEntityIDKey).(int)
+ if err := h.admin.Delete(ctx, n, id); err != nil {
+ msg.Error(ctx, err.Error())
+ return h.EntityDelete(n)(ctx)
+ }
+
+ msg.Success(ctx, fmt.Sprintf("Successfully deleted %s (ID %d).", n.GetName(), id))
+
+ return redirect.
+ New(ctx).
+ Route(routenames.AdminEntityList(n.GetName())).
+ StatusCode(http.StatusFound).
+ Go()
+ }
+}
+
+func (h *Admin) Backlite(handler func(http.ResponseWriter, *http.Request) error) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ if id := c.Param("id"); id != "" {
+ c.Request().SetPathValue("task", id)
+ }
+ return handler(c.Response().Writer, c.Request())
+ }
+}
diff --git a/pkg/handlers/auth.go b/pkg/handlers/auth.go
new file mode 100644
index 0000000..3bde378
--- /dev/null
+++ b/pkg/handlers/auth.go
@@ -0,0 +1,380 @@
+package handlers
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/ent/user"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "github.com/mikestefanello/pagoda/pkg/middleware"
+ "github.com/mikestefanello/pagoda/pkg/msg"
+ "github.com/mikestefanello/pagoda/pkg/redirect"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/pkg/ui/emails"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/pages"
+)
+
+type Auth struct {
+ config *config.Config
+ auth *services.AuthClient
+ mail *services.MailClient
+ orm *ent.Client
+}
+
+func init() {
+ Register(new(Auth))
+}
+
+func (h *Auth) Init(c *services.Container) error {
+ h.config = c.Config
+ h.orm = c.ORM
+ h.auth = c.Auth
+ h.mail = c.Mail
+ return nil
+}
+
+func (h *Auth) Routes(g *echo.Group) {
+ g.GET("/logout", h.Logout, middleware.RequireAuthentication).Name = routenames.Logout
+ g.GET("/email/verify/:token", h.VerifyEmail).Name = routenames.VerifyEmail
+
+ noAuth := g.Group("/user", middleware.RequireNoAuthentication)
+ noAuth.GET("/login", h.LoginPage).Name = routenames.Login
+ noAuth.POST("/login", h.LoginSubmit).Name = routenames.LoginSubmit
+ noAuth.GET("/register", h.RegisterPage).Name = routenames.Register
+ noAuth.POST("/register", h.RegisterSubmit).Name = routenames.RegisterSubmit
+ noAuth.GET("/password", h.ForgotPasswordPage).Name = routenames.ForgotPassword
+ noAuth.POST("/password", h.ForgotPasswordSubmit).Name = routenames.ForgotPasswordSubmit
+
+ resetGroup := noAuth.Group("/password/reset",
+ middleware.LoadUser(h.orm),
+ middleware.LoadValidPasswordToken(h.auth),
+ )
+ resetGroup.GET("/token/:user/:password_token/:token", h.ResetPasswordPage).Name = routenames.ResetPassword
+ resetGroup.POST("/token/:user/:password_token/:token", h.ResetPasswordSubmit).Name = routenames.ResetPasswordSubmit
+}
+
+func (h *Auth) ForgotPasswordPage(ctx echo.Context) error {
+ return pages.ForgotPassword(ctx, form.Get[forms.ForgotPassword](ctx))
+}
+
+func (h *Auth) ForgotPasswordSubmit(ctx echo.Context) error {
+ var input forms.ForgotPassword
+
+ succeed := func() error {
+ form.Clear(ctx)
+ msg.Success(ctx, "An email containing a link to reset your password will be sent to this address if it exists in our system.")
+ return h.ForgotPasswordPage(ctx)
+ }
+
+ err := form.Submit(ctx, &input)
+
+ switch err.(type) {
+ case nil:
+ case validator.ValidationErrors:
+ return h.ForgotPasswordPage(ctx)
+ default:
+ return err
+ }
+
+ // Attempt to load the user.
+ u, err := h.orm.User.
+ Query().
+ Where(user.Email(strings.ToLower(input.Email))).
+ Only(ctx.Request().Context())
+
+ switch err.(type) {
+ case *ent.NotFoundError:
+ return succeed()
+ case nil:
+ default:
+ return fail(err, "error querying user during forgot password")
+ }
+
+ // Generate the token.
+ token, pt, err := h.auth.GeneratePasswordResetToken(ctx, u.ID)
+ if err != nil {
+ return fail(err, "error generating password reset token")
+ }
+
+ log.Ctx(ctx).Info("generated password reset token",
+ "user_id", u.ID,
+ )
+
+ // Email the user.
+ url := ctx.Echo().Reverse(routenames.ResetPassword, u.ID, pt.ID, token)
+ err = h.mail.
+ Compose().
+ To(u.Email).
+ Subject("Reset your password").
+ Body(fmt.Sprintf("Go here to reset your password: %s", h.config.App.Host+url)).
+ Send(ctx)
+
+ if err != nil {
+ return fail(err, "error sending password reset email")
+ }
+
+ return succeed()
+}
+
+func (h *Auth) LoginPage(ctx echo.Context) error {
+ return pages.Login(ctx, form.Get[forms.Login](ctx))
+}
+
+func (h *Auth) LoginSubmit(ctx echo.Context) error {
+ var input forms.Login
+
+ authFailed := func() error {
+ input.SetFieldError("Email", "")
+ input.SetFieldError("Password", "")
+ msg.Error(ctx, "Invalid credentials. Please try again.")
+ return h.LoginPage(ctx)
+ }
+
+ err := form.Submit(ctx, &input)
+
+ switch err.(type) {
+ case nil:
+ case validator.ValidationErrors:
+ return h.LoginPage(ctx)
+ default:
+ return err
+ }
+
+ // Attempt to load the user.
+ u, err := h.orm.User.
+ Query().
+ Where(user.Email(strings.ToLower(input.Email))).
+ Only(ctx.Request().Context())
+
+ switch err.(type) {
+ case *ent.NotFoundError:
+ return authFailed()
+ case nil:
+ default:
+ return fail(err, "error querying user during login")
+ }
+
+ // Check if the password is correct.
+ err = h.auth.CheckPassword(input.Password, u.Password)
+ if err != nil {
+ return authFailed()
+ }
+
+ // Log the user in.
+ err = h.auth.Login(ctx, u.ID)
+ if err != nil {
+ return fail(err, "unable to log in user")
+ }
+
+ msg.Success(ctx, fmt.Sprintf("Welcome back, %s. You are now logged in.", u.Name))
+
+ return redirect.New(ctx).
+ Route(routenames.Home).
+ Go()
+}
+
+func (h *Auth) Logout(ctx echo.Context) error {
+ if err := h.auth.Logout(ctx); err == nil {
+ msg.Success(ctx, "You have been logged out successfully.")
+ } else {
+ msg.Error(ctx, "An error occurred. Please try again.")
+ }
+ return redirect.New(ctx).
+ Route(routenames.Home).
+ Go()
+}
+
+func (h *Auth) RegisterPage(ctx echo.Context) error {
+ return pages.Register(ctx, form.Get[forms.Register](ctx))
+}
+
+func (h *Auth) RegisterSubmit(ctx echo.Context) error {
+ var input forms.Register
+
+ err := form.Submit(ctx, &input)
+
+ switch err.(type) {
+ case nil:
+ case validator.ValidationErrors:
+ return h.RegisterPage(ctx)
+ default:
+ return err
+ }
+
+ // Attempt creating the user.
+ u, err := h.orm.User.
+ Create().
+ SetName(input.Name).
+ SetEmail(input.Email).
+ SetPassword(input.Password).
+ Save(ctx.Request().Context())
+
+ switch err.(type) {
+ case nil:
+ log.Ctx(ctx).Info("user created",
+ "user_name", u.Name,
+ "user_id", u.ID,
+ )
+ case *ent.ConstraintError:
+ msg.Warning(ctx, "A user with this email address already exists. Please log in.")
+ return redirect.New(ctx).
+ Route(routenames.Login).
+ Go()
+ default:
+ return fail(err, "unable to create user")
+ }
+
+ // Log the user in.
+ err = h.auth.Login(ctx, u.ID)
+ if err != nil {
+ log.Ctx(ctx).Error("unable to log user in",
+ "error", err,
+ "user_id", u.ID,
+ )
+ msg.Info(ctx, "Your account has been created.")
+ return redirect.New(ctx).
+ Route(routenames.Login).
+ Go()
+ }
+
+ msg.Success(ctx, "Your account has been created. You are now logged in.")
+
+ // Send the verification email.
+ h.sendVerificationEmail(ctx, u)
+
+ return redirect.New(ctx).
+ Route(routenames.Home).
+ Go()
+}
+
+func (h *Auth) sendVerificationEmail(ctx echo.Context, usr *ent.User) {
+ // Generate a token.
+ token, err := h.auth.GenerateEmailVerificationToken(usr.Email)
+ if err != nil {
+ log.Ctx(ctx).Error("unable to generate email verification token",
+ "user_id", usr.ID,
+ "error", err,
+ )
+ return
+ }
+
+ // Send the email.
+ err = h.mail.
+ Compose().
+ To(usr.Email).
+ Subject("Confirm your email address").
+ Component(emails.ConfirmEmailAddress(ctx, usr.Name, token)).
+ Send(ctx)
+
+ if err != nil {
+ log.Ctx(ctx).Error("unable to send email verification link",
+ "user_id", usr.ID,
+ "error", err,
+ )
+ return
+ }
+
+ msg.Info(ctx, "An email was sent to you to verify your email address.")
+}
+
+func (h *Auth) ResetPasswordPage(ctx echo.Context) error {
+ return pages.ResetPassword(ctx, form.Get[forms.ResetPassword](ctx))
+}
+
+func (h *Auth) ResetPasswordSubmit(ctx echo.Context) error {
+ var input forms.ResetPassword
+
+ err := form.Submit(ctx, &input)
+
+ switch err.(type) {
+ case nil:
+ case validator.ValidationErrors:
+ return h.ResetPasswordPage(ctx)
+ default:
+ return err
+ }
+
+ // Get the requesting user.
+ usr := ctx.Get(context.UserKey).(*ent.User)
+
+ // Update the user.
+ _, err = usr.
+ Update().
+ SetPassword(input.Password).
+ Save(ctx.Request().Context())
+
+ if err != nil {
+ return fail(err, "unable to update password")
+ }
+
+ // Delete all password tokens for this user.
+ err = h.auth.DeletePasswordTokens(ctx, usr.ID)
+ if err != nil {
+ return fail(err, "unable to delete password tokens")
+ }
+
+ msg.Success(ctx, "Your password has been updated.")
+ return redirect.New(ctx).
+ Route(routenames.Login).
+ Go()
+}
+
+func (h *Auth) VerifyEmail(ctx echo.Context) error {
+ var usr *ent.User
+
+ // Validate the token.
+ token := ctx.Param("token")
+ email, err := h.auth.ValidateEmailVerificationToken(token)
+ if err != nil {
+ msg.Warning(ctx, "The link is either invalid or has expired.")
+ return redirect.New(ctx).
+ Route(routenames.Home).
+ Go()
+ }
+
+ // Check if it matches the authenticated user.
+ if u := ctx.Get(context.AuthenticatedUserKey); u != nil {
+ authUser := u.(*ent.User)
+
+ if authUser.Email == email {
+ usr = authUser
+ }
+ }
+
+ // Query to find a matching user, if needed.
+ if usr == nil {
+ usr, err = h.orm.User.
+ Query().
+ Where(user.Email(email)).
+ Only(ctx.Request().Context())
+
+ if err != nil {
+ return fail(err, "query failed loading email verification token user")
+ }
+ }
+
+ // Verify the user, if needed.
+ if !usr.Verified {
+ usr, err = usr.
+ Update().
+ SetVerified(true).
+ Save(ctx.Request().Context())
+
+ if err != nil {
+ return fail(err, "failed to set user as verified")
+ }
+ }
+
+ msg.Success(ctx, "Your email has been successfully verified.")
+ return redirect.New(ctx).
+ Route(routenames.Home).
+ Go()
+}
diff --git a/pkg/handlers/cache.go b/pkg/handlers/cache.go
new file mode 100644
index 0000000..2b6d3b2
--- /dev/null
+++ b/pkg/handlers/cache.go
@@ -0,0 +1,76 @@
+package handlers
+
+import (
+ "errors"
+ "time"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/pages"
+)
+
+type Cache struct {
+ cache *services.CacheClient
+}
+
+func init() {
+ Register(new(Cache))
+}
+
+func (h *Cache) Init(c *services.Container) error {
+ h.cache = c.Cache
+ return nil
+}
+
+func (h *Cache) Routes(g *echo.Group) {
+ g.GET("/cache", h.Page).Name = routenames.Cache
+ g.POST("/cache", h.Submit).Name = routenames.CacheSubmit
+}
+
+func (h *Cache) Page(ctx echo.Context) error {
+ f := form.Get[forms.Cache](ctx)
+
+ // Fetch the value from the cache.
+ value, err := h.cache.
+ Get().
+ Key("page_cache_example").
+ Fetch(ctx.Request().Context())
+
+ // Store the value in the form, so it can be rendered, if found.
+ switch {
+ case err == nil:
+ f.CurrentValue = value.(string)
+ case errors.Is(err, services.ErrCacheMiss):
+ default:
+ return fail(err, "failed to fetch from cache")
+ }
+
+ return pages.UpdateCache(ctx, f)
+}
+
+func (h *Cache) Submit(ctx echo.Context) error {
+ var input forms.Cache
+
+ if err := form.Submit(ctx, &input); err != nil {
+ return err
+ }
+
+ // Set the cache.
+ err := h.cache.
+ Set().
+ Key("page_cache_example").
+ Data(input.Value).
+ Expiration(30 * time.Minute).
+ Save(ctx.Request().Context())
+
+ if err != nil {
+ return fail(err, "unable to set cache")
+ }
+
+ form.Clear(ctx)
+
+ return h.Page(ctx)
+}
diff --git a/pkg/handlers/contact.go b/pkg/handlers/contact.go
new file mode 100644
index 0000000..28fa0ae
--- /dev/null
+++ b/pkg/handlers/contact.go
@@ -0,0 +1,62 @@
+package handlers
+
+import (
+ "fmt"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/pages"
+)
+
+type Contact struct {
+ mail *services.MailClient
+}
+
+func init() {
+ Register(new(Contact))
+}
+
+func (h *Contact) Init(c *services.Container) error {
+ h.mail = c.Mail
+ return nil
+}
+
+func (h *Contact) Routes(g *echo.Group) {
+ g.GET("/contact", h.Page).Name = routenames.Contact
+ g.POST("/contact", h.Submit).Name = routenames.ContactSubmit
+}
+
+func (h *Contact) Page(ctx echo.Context) error {
+ return pages.ContactUs(ctx, form.Get[forms.Contact](ctx))
+}
+
+func (h *Contact) Submit(ctx echo.Context) error {
+ var input forms.Contact
+
+ err := form.Submit(ctx, &input)
+
+ switch err.(type) {
+ case nil:
+ case validator.ValidationErrors:
+ return h.Page(ctx)
+ default:
+ return err
+ }
+
+ err = h.mail.
+ Compose().
+ To(input.Email).
+ Subject("Contact form submitted").
+ Body(fmt.Sprintf("The message is: %s", input.Message)).
+ Send(ctx)
+
+ if err != nil {
+ return fail(err, "unable to send email")
+ }
+
+ return h.Page(ctx)
+}
diff --git a/pkg/handlers/error.go b/pkg/handlers/error.go
new file mode 100644
index 0000000..a225e04
--- /dev/null
+++ b/pkg/handlers/error.go
@@ -0,0 +1,43 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "github.com/mikestefanello/pagoda/pkg/ui/pages"
+)
+
+type Error struct{}
+
+func (e *Error) Page(err error, ctx echo.Context) {
+ if ctx.Response().Committed || context.IsCanceledError(err) {
+ return
+ }
+
+ // Determine the error status code.
+ code := http.StatusInternalServerError
+ if he, ok := err.(*echo.HTTPError); ok {
+ code = he.Code
+ }
+
+ // Log the error.
+ logger := log.Ctx(ctx)
+ switch {
+ case code >= 500:
+ logger.Error(err.Error())
+ case code >= 400:
+ logger.Warn(err.Error())
+ }
+
+ // Set the status code.
+ ctx.Response().WriteHeader(code)
+
+ // Render the error page.
+ if err = pages.Error(ctx, code); err != nil {
+ log.Ctx(ctx).Error("failed to render error page",
+ "error", err,
+ )
+ }
+}
diff --git a/pkg/handlers/files.go b/pkg/handlers/files.go
new file mode 100644
index 0000000..d2d5cd3
--- /dev/null
+++ b/pkg/handlers/files.go
@@ -0,0 +1,80 @@
+package handlers
+
+import (
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/msg"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/pkg/ui/models"
+ "github.com/mikestefanello/pagoda/pkg/ui/pages"
+ "github.com/spf13/afero"
+)
+
+type Files struct {
+ files afero.Fs
+}
+
+func init() {
+ Register(new(Files))
+}
+
+func (h *Files) Init(c *services.Container) error {
+ h.files = c.Files
+ return nil
+}
+
+func (h *Files) Routes(g *echo.Group) {
+ g.GET("/files", h.Page).Name = routenames.Files
+ g.POST("/files", h.Submit).Name = routenames.FilesSubmit
+}
+
+func (h *Files) Page(ctx echo.Context) error {
+ // Compile a list of all uploaded files to be rendered.
+ info, err := afero.ReadDir(h.files, "")
+ if err != nil {
+ return err
+ }
+
+ files := make([]*models.File, 0)
+ for _, file := range info {
+ files = append(files, &models.File{
+ Name: file.Name(),
+ Size: file.Size(),
+ Modified: file.ModTime().Format(time.DateTime),
+ })
+ }
+
+ return pages.UploadFile(ctx, files)
+}
+
+func (h *Files) Submit(ctx echo.Context) error {
+ file, err := ctx.FormFile("file")
+ if err != nil {
+ msg.Error(ctx, "A file is required.")
+ return h.Page(ctx)
+ }
+
+ src, err := file.Open()
+ if err != nil {
+ return err
+ }
+ defer src.Close()
+
+ dst, err := h.files.Create(file.Filename)
+ if err != nil {
+ return err
+ }
+ defer dst.Close()
+
+ if _, err = io.Copy(dst, src); err != nil {
+ return err
+ }
+
+ msg.Success(ctx, fmt.Sprintf("%s was uploaded successfully.", file.Filename))
+
+ return h.Page(ctx)
+}
diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go
new file mode 100644
index 0000000..d5c1c0d
--- /dev/null
+++ b/pkg/handlers/handlers.go
@@ -0,0 +1,36 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/services"
+)
+
+var handlers []Handler
+
+// Handler handles one or more HTTP routes
+type Handler interface {
+ // Routes allows for self-registration of HTTP routes on the router
+ Routes(g *echo.Group)
+
+ // Init provides the service container to initialize
+ Init(*services.Container) error
+}
+
+// Register registers a handler
+func Register(h Handler) {
+ handlers = append(handlers, h)
+}
+
+// GetHandlers returns all handlers
+func GetHandlers() []Handler {
+ return handlers
+}
+
+// fail is a helper to fail a request by returning a 500 error and logging the error
+func fail(err error, log string) error {
+ // The error handler will handle logging
+ return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("%s: %v", log, err))
+}
diff --git a/pkg/handlers/handlers_test.go b/pkg/handlers/handlers_test.go
new file mode 100644
index 0000000..d551e4b
--- /dev/null
+++ b/pkg/handlers/handlers_test.go
@@ -0,0 +1,29 @@
+package handlers
+
+import (
+ "errors"
+ "net/http"
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetSetHandlers(t *testing.T) {
+ handlers = []Handler{}
+ assert.Empty(t, GetHandlers())
+ h := new(Pages)
+ Register(h)
+ got := GetHandlers()
+ require.Len(t, got, 1)
+ assert.Equal(t, h, got[0])
+}
+
+func TestFail(t *testing.T) {
+ err := fail(errors.New("err message"), "log message")
+ require.IsType(t, new(echo.HTTPError), err)
+ he := err.(*echo.HTTPError)
+ assert.Equal(t, http.StatusInternalServerError, he.Code)
+ assert.Equal(t, "log message: err message", he.Message)
+}
diff --git a/pkg/handlers/pages.go b/pkg/handlers/pages.go
new file mode 100644
index 0000000..832d70f
--- /dev/null
+++ b/pkg/handlers/pages.go
@@ -0,0 +1,55 @@
+package handlers
+
+import (
+ "fmt"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/pager"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/pkg/ui/models"
+ "github.com/mikestefanello/pagoda/pkg/ui/pages"
+)
+
+type Pages struct{}
+
+func init() {
+ Register(new(Pages))
+}
+
+func (h *Pages) Init(c *services.Container) error {
+ return nil
+}
+
+func (h *Pages) Routes(g *echo.Group) {
+ g.GET("/", h.Home).Name = routenames.Home
+ g.GET("/about", h.About).Name = routenames.About
+}
+
+func (h *Pages) Home(ctx echo.Context) error {
+ pgr := pager.NewPager(ctx, 4)
+
+ return pages.Home(ctx, &models.Posts{
+ Posts: h.fetchPosts(&pgr),
+ Pager: pgr,
+ })
+}
+
+// fetchPosts is a mock example of fetching posts to illustrate how paging works.
+func (h *Pages) fetchPosts(pager *pager.Pager) []models.Post {
+ pager.SetItems(20)
+ posts := make([]models.Post, 20)
+
+ for k := range posts {
+ posts[k] = models.Post{
+ ID: k + 1,
+ Title: fmt.Sprintf("Post example #%d", k+1),
+ Body: fmt.Sprintf("Lorem ipsum example #%d ddolor sit amet, consectetur adipiscing elit. Nam elementum vulputate tristique.", k+1),
+ }
+ }
+ return posts[pager.GetOffset() : pager.GetOffset()+pager.ItemsPerPage]
+}
+
+func (h *Pages) About(ctx echo.Context) error {
+ return pages.About(ctx)
+}
diff --git a/routes/about_test.go b/pkg/handlers/pages_test.go
similarity index 71%
rename from routes/about_test.go
rename to pkg/handlers/pages_test.go
index 359a737..5013793 100644
--- a/routes/about_test.go
+++ b/pkg/handlers/pages_test.go
@@ -1,23 +1,24 @@
-package routes
+package handlers
import (
"net/http"
"testing"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
"github.com/stretchr/testify/assert"
)
// Simple example of how to test routes and their markup using the test HTTP server spun up within
// this test package
-func TestAbout_Get(t *testing.T) {
+func TestPages__About(t *testing.T) {
doc := request(t).
- setRoute("about").
+ setRoute(routenames.About).
get().
assertStatusCode(http.StatusOK).
toDoc()
// Goquery is an excellent package to use for testing HTML markup
- h1 := doc.Find("h1.title")
+ h1 := doc.Find("h1")
assert.Len(t, h1.Nodes, 1)
assert.Equal(t, "About", h1.Text())
}
diff --git a/pkg/handlers/router.go b/pkg/handlers/router.go
new file mode 100644
index 0000000..a58f802
--- /dev/null
+++ b/pkg/handlers/router.go
@@ -0,0 +1,93 @@
+package handlers
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo/v4"
+ echomw "github.com/labstack/echo/v4/middleware"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/middleware"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ files "github.com/mikestefanello/pagoda/public"
+)
+
+// BuildRouter builds the router.
+func BuildRouter(c *services.Container) error {
+ // Force HTTPS, if enabled.
+ if c.Config.HTTP.TLS.Enabled {
+ c.Web.Use(echomw.HTTPSRedirect())
+ }
+
+ // Serve public files with cache control.
+ c.Web.Group("", middleware.CacheControl(c.Config.Cache.Expiration.PublicFile)).
+ Static("files", "public/files")
+
+ // Serve static files.
+ // ui.StaticFile() should be used in ui components to append a cache key to the URL to break cache
+ // after each server reboot.
+ c.Web.Group(
+ "",
+ echomw.GzipWithConfig(echomw.GzipConfig{
+ Skipper: func(c echo.Context) bool {
+ for _, ext := range []string{
+ ".js",
+ ".css",
+ } {
+ if strings.HasSuffix(c.Request().URL.Path, ext) {
+ return false
+ }
+ }
+ return true
+ },
+ }),
+ middleware.CacheControl(c.Config.Cache.Expiration.PublicFile),
+ ).StaticFS("static", echo.MustSubFS(files.Static, "static"))
+
+ // Non-static file route group.
+ g := c.Web.Group("")
+
+ // Create a cookie store for session data.
+ cookieStore := sessions.NewCookieStore([]byte(c.Config.App.EncryptionKey))
+ cookieStore.Options.HttpOnly = true
+ cookieStore.Options.SameSite = http.SameSiteStrictMode
+
+ g.Use(
+ echomw.RemoveTrailingSlashWithConfig(echomw.TrailingSlashConfig{
+ RedirectCode: http.StatusMovedPermanently,
+ }),
+ echomw.Recover(),
+ echomw.Secure(),
+ echomw.RequestID(),
+ middleware.SetLogger(),
+ middleware.LogRequest(),
+ echomw.Gzip(),
+ echomw.TimeoutWithConfig(echomw.TimeoutConfig{
+ Timeout: c.Config.App.Timeout,
+ }),
+ middleware.Config(c.Config),
+ middleware.Session(cookieStore),
+ middleware.LoadAuthenticatedUser(c.Auth),
+ echomw.CSRFWithConfig(echomw.CSRFConfig{
+ TokenLookup: "form:csrf",
+ CookieHTTPOnly: true,
+ CookieSameSite: http.SameSiteStrictMode,
+ ContextKey: context.CSRFKey,
+ }),
+ )
+
+ // Error handler.
+ c.Web.HTTPErrorHandler = new(Error).Page
+
+ // Initialize and register all handlers.
+ for _, h := range GetHandlers() {
+ if err := h.Init(c); err != nil {
+ return err
+ }
+
+ h.Routes(g)
+ }
+
+ return nil
+}
diff --git a/routes/routes_test.go b/pkg/handlers/router_test.go
similarity index 68%
rename from routes/routes_test.go
rename to pkg/handlers/router_test.go
index b7552be..002d077 100644
--- a/routes/routes_test.go
+++ b/pkg/handlers/router_test.go
@@ -1,14 +1,15 @@
-package routes
+package handlers
import (
"net/http"
+ "net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"testing"
- "goweb/config"
- "goweb/services"
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/pkg/services"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
@@ -27,19 +28,22 @@ func TestMain(m *testing.M) {
// Start a new container
c = services.NewContainer()
- defer func() {
- if err := c.Shutdown(); err != nil {
- c.Web.Logger.Fatal(err)
- }
- }()
// Start a test HTTP server
- BuildRouter(c)
+ if err := BuildRouter(c); err != nil {
+ panic(err)
+ }
srv = httptest.NewServer(c.Web)
// Run tests
exitVal := m.Run()
+
+ // Shutdown the container and test server
+ if err := c.Shutdown(); err != nil {
+ panic(err)
+ }
srv.Close()
+
os.Exit(exitVal)
}
@@ -51,8 +55,14 @@ type httpRequest struct {
}
func request(t *testing.T) *httpRequest {
+ jar, err := cookiejar.New(nil)
+ require.NoError(t, err)
r := httpRequest{
- t: t,
+ t: t,
+ body: url.Values{},
+ client: http.Client{
+ Jar: jar,
+ },
}
return &r
}
@@ -62,7 +72,7 @@ func (h *httpRequest) setClient(client http.Client) *httpRequest {
return h
}
-func (h *httpRequest) setRoute(route string, params ...interface{}) *httpRequest {
+func (h *httpRequest) setRoute(route string, params ...any) *httpRequest {
h.route = srv.URL + c.Web.Reverse(route, params)
return h
}
@@ -83,6 +93,18 @@ func (h *httpRequest) get() *httpResponse {
}
func (h *httpRequest) post() *httpResponse {
+ // Make a get request to get the CSRF token
+ doc := h.get().
+ assertStatusCode(http.StatusOK).
+ toDoc()
+
+ // Extract the CSRF and include it in the POST request body
+ csrf := doc.Find(`input[name="csrf"]`).First()
+ token, exists := csrf.Attr("value")
+ assert.True(h.t, exists)
+ h.body["csrf"] = []string{token}
+
+ // Make the POST requests
resp, err := h.client.PostForm(h.route, h.body)
require.NoError(h.t, err)
r := httpResponse{
@@ -102,7 +124,7 @@ func (h *httpResponse) assertStatusCode(code int) *httpResponse {
return h
}
-func (h *httpResponse) assertRedirect(t *testing.T, route string, params ...interface{}) *httpResponse {
+func (h *httpResponse) assertRedirect(t *testing.T, route string, params ...any) *httpResponse {
assert.Equal(t, c.Web.Reverse(route, params), h.Header.Get("Location"))
return h
}
diff --git a/pkg/handlers/search.go b/pkg/handlers/search.go
new file mode 100644
index 0000000..f0e92ca
--- /dev/null
+++ b/pkg/handlers/search.go
@@ -0,0 +1,44 @@
+package handlers
+
+import (
+ "fmt"
+ "math/rand"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/pkg/ui/models"
+ "github.com/mikestefanello/pagoda/pkg/ui/pages"
+)
+
+type Search struct{}
+
+func init() {
+ Register(new(Search))
+}
+
+func (h *Search) Init(c *services.Container) error {
+ return nil
+}
+
+func (h *Search) Routes(g *echo.Group) {
+ g.GET("/search", h.Page).Name = routenames.Search
+}
+
+func (h *Search) Page(ctx echo.Context) error {
+ // Fake search results.
+ results := make([]*models.SearchResult, 0, 5)
+ if search := ctx.QueryParam("query"); search != "" {
+ for i := 0; i < 5; i++ {
+ title := "Lorem ipsum example ddolor sit amet"
+ index := rand.Intn(len(title))
+ title = title[:index] + search + title[index:]
+ results = append(results, &models.SearchResult{
+ Title: title,
+ URL: fmt.Sprintf("https://www.%s.com", search),
+ })
+ }
+ }
+
+ return pages.SearchResults(ctx, results)
+}
diff --git a/pkg/handlers/task.go b/pkg/handlers/task.go
new file mode 100644
index 0000000..cd5cc66
--- /dev/null
+++ b/pkg/handlers/task.go
@@ -0,0 +1,71 @@
+package handlers
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/mikestefanello/backlite"
+ "github.com/mikestefanello/pagoda/pkg/msg"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/pages"
+
+ "github.com/go-playground/validator/v10"
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/pkg/tasks"
+)
+
+type Task struct {
+ tasks *backlite.Client
+}
+
+func init() {
+ Register(new(Task))
+}
+
+func (h *Task) Init(c *services.Container) error {
+ h.tasks = c.Tasks
+ return nil
+}
+
+func (h *Task) Routes(g *echo.Group) {
+ g.GET("/task", h.Page).Name = routenames.Task
+ g.POST("/task", h.Submit).Name = routenames.TaskSubmit
+}
+
+func (h *Task) Page(ctx echo.Context) error {
+ return pages.AddTask(ctx, form.Get[forms.Task](ctx))
+}
+
+func (h *Task) Submit(ctx echo.Context) error {
+ var input forms.Task
+
+ err := form.Submit(ctx, &input)
+
+ switch err.(type) {
+ case nil:
+ case validator.ValidationErrors:
+ return h.Page(ctx)
+ default:
+ return err
+ }
+
+ // Insert the task
+ _, err = h.tasks.
+ Add(tasks.ExampleTask{
+ Message: input.Message,
+ }).
+ Wait(time.Duration(input.Delay) * time.Second).
+ Save()
+
+ if err != nil {
+ return fail(err, "unable to create a task")
+ }
+
+ msg.Success(ctx, fmt.Sprintf("The task has been created. Check the logs in %d seconds.", input.Delay))
+ form.Clear(ctx)
+
+ return h.Page(ctx)
+}
diff --git a/pkg/htmx/htmx.go b/pkg/htmx/htmx.go
new file mode 100644
index 0000000..e897c42
--- /dev/null
+++ b/pkg/htmx/htmx.go
@@ -0,0 +1,97 @@
+package htmx
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/context"
+)
+
+// Request headers: https://htmx.org/docs/#request-headers
+const (
+ HeaderBoosted = "HX-Boosted"
+ HeaderHistoryRestoreRequest = "HX-History-Restore-Request"
+ HeaderPrompt = "HX-Prompt"
+ HeaderRequest = "HX-Request"
+ HeaderTarget = "HX-Target"
+ HeaderTrigger = "HX-Trigger"
+ HeaderTriggerName = "HX-Trigger-Name"
+)
+
+// Response headers: https://htmx.org/docs/#response-headers
+const (
+ HeaderPushURL = "HX-Push-Url"
+ HeaderRedirect = "HX-Redirect"
+ HeaderReplaceURL = "HX-Replace-Url"
+ HeaderRefresh = "HX-Refresh"
+ HeaderTriggerAfterSettle = "HX-Trigger-After-Settle"
+ HeaderTriggerAfterSwap = "HX-Trigger-After-Swap"
+)
+
+type (
+ // Request contains data that HTMX provides during requests.
+ Request struct {
+ Enabled bool
+ Boosted bool
+ HistoryRestore bool
+ Trigger string
+ TriggerName string
+ Target string
+ Prompt string
+ }
+
+ // Response contain data that the server can communicate back to HTMX.
+ Response struct {
+ PushURL string
+ Redirect string
+ Refresh bool
+ ReplaceURL string
+ Trigger string
+ TriggerAfterSwap string
+ TriggerAfterSettle string
+ NoContent bool
+ }
+)
+
+// GetRequest extracts HTMX data from the request,
+func GetRequest(ctx echo.Context) *Request {
+ return context.Cache(ctx, context.HTMXRequestKey, func(ctx echo.Context) *Request {
+ return &Request{
+ Enabled: ctx.Request().Header.Get(HeaderRequest) == "true",
+ Boosted: ctx.Request().Header.Get(HeaderBoosted) == "true",
+ Trigger: ctx.Request().Header.Get(HeaderTrigger),
+ TriggerName: ctx.Request().Header.Get(HeaderTriggerName),
+ Target: ctx.Request().Header.Get(HeaderTarget),
+ Prompt: ctx.Request().Header.Get(HeaderPrompt),
+ HistoryRestore: ctx.Request().Header.Get(HeaderHistoryRestoreRequest) == "true",
+ }
+ })
+}
+
+// Apply applies data from a Response to a server response.
+func (r Response) Apply(ctx echo.Context) {
+ if r.PushURL != "" {
+ ctx.Response().Header().Set(HeaderPushURL, r.PushURL)
+ }
+ if r.Redirect != "" {
+ ctx.Response().Header().Set(HeaderRedirect, r.Redirect)
+ }
+ if r.Refresh {
+ ctx.Response().Header().Set(HeaderRefresh, "true")
+ }
+ if r.Trigger != "" {
+ ctx.Response().Header().Set(HeaderTrigger, r.Trigger)
+ }
+ if r.TriggerAfterSwap != "" {
+ ctx.Response().Header().Set(HeaderTriggerAfterSwap, r.TriggerAfterSwap)
+ }
+ if r.TriggerAfterSettle != "" {
+ ctx.Response().Header().Set(HeaderTriggerAfterSettle, r.TriggerAfterSettle)
+ }
+ if r.ReplaceURL != "" {
+ ctx.Response().Header().Set(HeaderReplaceURL, r.ReplaceURL)
+ }
+ if r.NoContent {
+ ctx.Response().Status = http.StatusNoContent
+ }
+}
diff --git a/htmx/htmx_test.go b/pkg/htmx/htmx_test.go
similarity index 73%
rename from htmx/htmx_test.go
rename to pkg/htmx/htmx_test.go
index 09a957f..4963197 100644
--- a/htmx/htmx_test.go
+++ b/pkg/htmx/htmx_test.go
@@ -4,7 +4,8 @@ import (
"net/http"
"testing"
- "goweb/tests"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/tests"
"github.com/stretchr/testify/assert"
@@ -19,21 +20,29 @@ func TestSetRequest(t *testing.T) {
ctx.Request().Header.Set(HeaderTriggerName, "b")
ctx.Request().Header.Set(HeaderTarget, "c")
ctx.Request().Header.Set(HeaderPrompt, "d")
+ ctx.Request().Header.Set(HeaderHistoryRestoreRequest, "true")
r := GetRequest(ctx)
assert.Equal(t, true, r.Enabled)
assert.Equal(t, true, r.Boosted)
+ assert.Equal(t, true, r.HistoryRestore)
assert.Equal(t, "a", r.Trigger)
assert.Equal(t, "b", r.TriggerName)
assert.Equal(t, "c", r.Target)
assert.Equal(t, "d", r.Prompt)
+
+ cached := context.Cache(ctx, context.HTMXRequestKey, func(ctx echo.Context) *Request {
+ return nil
+ })
+ assert.Equal(t, r, cached)
}
func TestResponse_Apply(t *testing.T) {
ctx, _ := tests.NewContext(echo.New(), "/")
r := Response{
- Push: "a",
+ PushURL: "a",
Redirect: "b",
+ ReplaceURL: "f",
Refresh: true,
Trigger: "c",
TriggerAfterSwap: "d",
@@ -42,11 +51,12 @@ func TestResponse_Apply(t *testing.T) {
}
r.Apply(ctx)
- assert.Equal(t, "a", ctx.Response().Header().Get(HeaderPush))
+ assert.Equal(t, "a", ctx.Response().Header().Get(HeaderPushURL))
assert.Equal(t, "b", ctx.Response().Header().Get(HeaderRedirect))
assert.Equal(t, "true", ctx.Response().Header().Get(HeaderRefresh))
assert.Equal(t, "c", ctx.Response().Header().Get(HeaderTrigger))
assert.Equal(t, "d", ctx.Response().Header().Get(HeaderTriggerAfterSwap))
assert.Equal(t, "e", ctx.Response().Header().Get(HeaderTriggerAfterSettle))
+ assert.Equal(t, "f", ctx.Response().Header().Get(HeaderReplaceURL))
assert.Equal(t, http.StatusNoContent, ctx.Response().Status)
}
diff --git a/pkg/log/log.go b/pkg/log/log.go
new file mode 100644
index 0000000..51c6448
--- /dev/null
+++ b/pkg/log/log.go
@@ -0,0 +1,27 @@
+package log
+
+import (
+ "log/slog"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/context"
+)
+
+// Set sets a logger in the context.
+func Set(ctx echo.Context, logger *slog.Logger) {
+ ctx.Set(context.LoggerKey, logger)
+}
+
+// Ctx returns the logger stored in context, or provides the default logger if one is not present.
+func Ctx(ctx echo.Context) *slog.Logger {
+ if l, ok := ctx.Get(context.LoggerKey).(*slog.Logger); ok {
+ return l
+ }
+
+ return Default()
+}
+
+// Default returns the default logger.
+func Default() *slog.Logger {
+ return slog.Default()
+}
diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go
new file mode 100644
index 0000000..d9486a8
--- /dev/null
+++ b/pkg/log/log_test.go
@@ -0,0 +1,21 @@
+package log
+
+import (
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCtxSet(t *testing.T) {
+ ctx, _ := tests.NewContext(echo.New(), "/")
+ logger := Ctx(ctx)
+ assert.NotNil(t, logger)
+
+ logger = logger.With("a", "b")
+ Set(ctx, logger)
+
+ got := Ctx(ctx)
+ assert.Equal(t, got, logger)
+}
diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go
new file mode 100644
index 0000000..bd58912
--- /dev/null
+++ b/pkg/middleware/auth.go
@@ -0,0 +1,120 @@
+package middleware
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "github.com/mikestefanello/pagoda/pkg/msg"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/services"
+
+ "github.com/labstack/echo/v4"
+)
+
+// LoadAuthenticatedUser loads the authenticated user, if one, and stores in context.
+func LoadAuthenticatedUser(authClient *services.AuthClient) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ u, err := authClient.GetAuthenticatedUser(c)
+ switch err.(type) {
+ case *ent.NotFoundError:
+ log.Ctx(c).Warn("auth user not found")
+ case services.NotAuthenticatedError:
+ case nil:
+ c.Set(context.AuthenticatedUserKey, u)
+ default:
+ return echo.NewHTTPError(
+ http.StatusInternalServerError,
+ fmt.Sprintf("error querying for authenticated user: %v", err),
+ )
+ }
+
+ return next(c)
+ }
+ }
+}
+
+// LoadValidPasswordToken loads a valid password token entity that matches the user and token
+// provided in path parameters
+// If the token is invalid, the user will be redirected to the forgot password route
+// This requires that the user owning the token is loaded in to context.
+func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ // Extract the user parameter
+ if c.Get(context.UserKey) == nil {
+ return echo.NewHTTPError(http.StatusInternalServerError)
+ }
+ usr := c.Get(context.UserKey).(*ent.User)
+
+ // Extract the token ID.
+ tokenID, err := strconv.Atoi(c.Param("password_token"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusNotFound)
+ }
+
+ // Attempt to load a valid password token.
+ token, err := authClient.GetValidPasswordToken(
+ c,
+ usr.ID,
+ tokenID,
+ c.Param("token"),
+ )
+
+ switch err.(type) {
+ case nil:
+ c.Set(context.PasswordTokenKey, token)
+ return next(c)
+ case services.InvalidPasswordTokenError:
+ msg.Warning(c, "The link is either invalid or has expired. Please request a new one.")
+ return c.Redirect(http.StatusFound, c.Echo().Reverse(routenames.ForgotPassword))
+ default:
+ return echo.NewHTTPError(
+ http.StatusInternalServerError,
+ fmt.Sprintf("error loading password token: %v", err),
+ )
+ }
+ }
+ }
+}
+
+// RequireAuthentication requires that the user be authenticated in order to proceed.
+func RequireAuthentication(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ if u := c.Get(context.AuthenticatedUserKey); u == nil {
+ return echo.NewHTTPError(http.StatusUnauthorized)
+ }
+
+ return next(c)
+ }
+}
+
+// RequireNoAuthentication requires that the user not be authenticated in order to proceed.
+func RequireNoAuthentication(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ if u := c.Get(context.AuthenticatedUserKey); u != nil {
+ return echo.NewHTTPError(http.StatusForbidden)
+ }
+
+ return next(c)
+ }
+}
+
+// RequireAdmin requires that the authenticated user be an admin in order to proceed.
+func RequireAdmin(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ if u := c.Get(context.AuthenticatedUserKey); u != nil {
+ if user, ok := u.(*ent.User); ok {
+ if user.Admin {
+ return next(c)
+ }
+ }
+ }
+
+ return echo.NewHTTPError(http.StatusUnauthorized)
+ }
+}
diff --git a/middleware/auth_test.go b/pkg/middleware/auth_test.go
similarity index 61%
rename from middleware/auth_test.go
rename to pkg/middleware/auth_test.go
index d74dd42..01e9161 100644
--- a/middleware/auth_test.go
+++ b/pkg/middleware/auth_test.go
@@ -1,13 +1,14 @@
package middleware
import (
+ goctx "context"
"fmt"
"net/http"
"testing"
- "goweb/context"
- "goweb/ent"
- "goweb/tests"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/tests"
"github.com/stretchr/testify/require"
@@ -40,7 +41,7 @@ func TestRequireAuthentication(t *testing.T) {
tests.InitSession(ctx)
// Not logged in
- err := tests.ExecuteMiddleware(ctx, RequireAuthentication())
+ err := tests.ExecuteMiddleware(ctx, RequireAuthentication)
tests.AssertHTTPErrorCode(t, err, http.StatusUnauthorized)
// Login
@@ -49,7 +50,7 @@ func TestRequireAuthentication(t *testing.T) {
_ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth))
// Logged in
- err = tests.ExecuteMiddleware(ctx, RequireAuthentication())
+ err = tests.ExecuteMiddleware(ctx, RequireAuthentication)
assert.Nil(t, err)
}
@@ -58,7 +59,7 @@ func TestRequireNoAuthentication(t *testing.T) {
tests.InitSession(ctx)
// Not logged in
- err := tests.ExecuteMiddleware(ctx, RequireNoAuthentication())
+ err := tests.ExecuteMiddleware(ctx, RequireNoAuthentication)
assert.Nil(t, err)
// Login
@@ -67,7 +68,7 @@ func TestRequireNoAuthentication(t *testing.T) {
_ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth))
// Logged in
- err = tests.ExecuteMiddleware(ctx, RequireNoAuthentication())
+ err = tests.ExecuteMiddleware(ctx, RequireNoAuthentication)
tests.AssertHTTPErrorCode(t, err, http.StatusForbidden)
}
@@ -79,17 +80,17 @@ func TestLoadValidPasswordToken(t *testing.T) {
err := tests.ExecuteMiddleware(ctx, LoadValidPasswordToken(c.Auth))
tests.AssertHTTPErrorCode(t, err, http.StatusInternalServerError)
- // Add user context but no password token and expect a redirect
- ctx.SetParamNames("user")
- ctx.SetParamValues(fmt.Sprintf("%d", usr.ID))
+ // Add user and password token context but no token and expect a redirect
+ ctx.SetParamNames("user", "password_token")
+ ctx.SetParamValues(fmt.Sprintf("%d", usr.ID), "1")
_ = tests.ExecuteMiddleware(ctx, LoadUser(c.ORM))
err = tests.ExecuteMiddleware(ctx, LoadValidPasswordToken(c.Auth))
assert.NoError(t, err)
assert.Equal(t, http.StatusFound, ctx.Response().Status)
// Add user context and invalid password token and expect a redirect
- ctx.SetParamNames("user", "password_token")
- ctx.SetParamValues(fmt.Sprintf("%d", usr.ID), "faketoken")
+ ctx.SetParamNames("user", "password_token", "token")
+ ctx.SetParamValues(fmt.Sprintf("%d", usr.ID), "1", "faketoken")
_ = tests.ExecuteMiddleware(ctx, LoadUser(c.ORM))
err = tests.ExecuteMiddleware(ctx, LoadValidPasswordToken(c.Auth))
assert.NoError(t, err)
@@ -100,8 +101,8 @@ func TestLoadValidPasswordToken(t *testing.T) {
require.NoError(t, err)
// Add user and valid password token
- ctx.SetParamNames("user", "password_token")
- ctx.SetParamValues(fmt.Sprintf("%d", usr.ID), token)
+ ctx.SetParamNames("user", "password_token", "token")
+ ctx.SetParamValues(fmt.Sprintf("%d", usr.ID), fmt.Sprintf("%d", pt.ID), token)
_ = tests.ExecuteMiddleware(ctx, LoadUser(c.ORM))
err = tests.ExecuteMiddleware(ctx, LoadValidPasswordToken(c.Auth))
assert.Nil(t, err)
@@ -109,3 +110,36 @@ func TestLoadValidPasswordToken(t *testing.T) {
require.True(t, ok)
assert.Equal(t, pt.ID, ctxPt.ID)
}
+
+func TestRequireAdmin(t *testing.T) {
+ ctx, _ := tests.NewContext(c.Web, "/")
+ tests.InitSession(ctx)
+
+ // Not logged in
+ err := tests.ExecuteMiddleware(ctx, RequireAdmin)
+ tests.AssertHTTPErrorCode(t, err, http.StatusUnauthorized)
+
+ // Login as a non-admin
+ err = c.Auth.Login(ctx, usr.ID)
+ require.NoError(t, err)
+ _ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth))
+
+ // Logged in as a non-admin
+ err = tests.ExecuteMiddleware(ctx, RequireAdmin)
+ tests.AssertHTTPErrorCode(t, err, http.StatusUnauthorized)
+
+ // Create an admin and login
+ adm, err := tests.CreateUser(c.ORM)
+ require.NoError(t, err)
+ err = c.ORM.User.Update().
+ SetAdmin(true).
+ Exec(goctx.Background())
+ require.NoError(t, err)
+ err = c.Auth.Login(ctx, adm.ID)
+ require.NoError(t, err)
+ _ = tests.ExecuteMiddleware(ctx, LoadAuthenticatedUser(c.Auth))
+
+ // Logged in as an admin
+ err = tests.ExecuteMiddleware(ctx, RequireAdmin)
+ assert.Nil(t, err)
+}
diff --git a/pkg/middleware/cache.go b/pkg/middleware/cache.go
new file mode 100644
index 0000000..adfb8eb
--- /dev/null
+++ b/pkg/middleware/cache.go
@@ -0,0 +1,22 @@
+package middleware
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/labstack/echo/v4"
+)
+
+// CacheControl sets a Cache-Control header with a given max age.
+func CacheControl(maxAge time.Duration) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ v := "no-cache, no-store"
+ if maxAge > 0 {
+ v = fmt.Sprintf("public, max-age=%.0f", maxAge.Seconds())
+ }
+ ctx.Response().Header().Set("Cache-Control", v)
+ return next(ctx)
+ }
+ }
+}
diff --git a/pkg/middleware/cache_test.go b/pkg/middleware/cache_test.go
new file mode 100644
index 0000000..040777d
--- /dev/null
+++ b/pkg/middleware/cache_test.go
@@ -0,0 +1,18 @@
+package middleware
+
+import (
+ "testing"
+ "time"
+
+ "github.com/mikestefanello/pagoda/pkg/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCacheControl(t *testing.T) {
+ ctx, _ := tests.NewContext(c.Web, "/")
+ _ = tests.ExecuteMiddleware(ctx, CacheControl(time.Second*5))
+ assert.Equal(t, "public, max-age=5", ctx.Response().Header().Get("Cache-Control"))
+ _ = tests.ExecuteMiddleware(ctx, CacheControl(0))
+ assert.Equal(t, "no-cache, no-store", ctx.Response().Header().Get("Cache-Control"))
+}
diff --git a/pkg/middleware/config.go b/pkg/middleware/config.go
new file mode 100644
index 0000000..e3c89c1
--- /dev/null
+++ b/pkg/middleware/config.go
@@ -0,0 +1,17 @@
+package middleware
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/pkg/context"
+)
+
+// Config stores the configuration in the request so it can be accessed by the ui.
+func Config(cfg *config.Config) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ ctx.Set(context.ConfigKey, cfg)
+ return next(ctx)
+ }
+ }
+}
diff --git a/pkg/middleware/config_test.go b/pkg/middleware/config_test.go
new file mode 100644
index 0000000..22c0283
--- /dev/null
+++ b/pkg/middleware/config_test.go
@@ -0,0 +1,22 @@
+package middleware
+
+import (
+ "testing"
+
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/tests"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestConfig(t *testing.T) {
+ ctx, _ := tests.NewContext(c.Web, "/")
+ cfg := &config.Config{}
+ err := tests.ExecuteMiddleware(ctx, Config(cfg))
+ require.NoError(t, err)
+
+ got, ok := ctx.Get(context.ConfigKey).(*config.Config)
+ require.True(t, ok)
+ assert.Same(t, got, cfg)
+}
diff --git a/middleware/entity.go b/pkg/middleware/entity.go
similarity index 72%
rename from middleware/entity.go
rename to pkg/middleware/entity.go
index a9fa2af..d6a4c01 100644
--- a/middleware/entity.go
+++ b/pkg/middleware/entity.go
@@ -1,17 +1,18 @@
package middleware
import (
+ "fmt"
"net/http"
"strconv"
- "goweb/context"
- "goweb/ent"
- "goweb/ent/user"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/ent/user"
+ "github.com/mikestefanello/pagoda/pkg/context"
"github.com/labstack/echo/v4"
)
-// LoadUser loads the user based on the ID provided as a path parameter
+// LoadUser loads the user based on the ID provided as a path parameter.
func LoadUser(orm *ent.Client) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
@@ -32,8 +33,10 @@ func LoadUser(orm *ent.Client) echo.MiddlewareFunc {
case *ent.NotFoundError:
return echo.NewHTTPError(http.StatusNotFound)
default:
- c.Logger().Error(err)
- return echo.NewHTTPError(http.StatusInternalServerError)
+ return echo.NewHTTPError(
+ http.StatusInternalServerError,
+ fmt.Sprintf("error querying user: %v", err),
+ )
}
}
}
diff --git a/middleware/entity_test.go b/pkg/middleware/entity_test.go
similarity index 76%
rename from middleware/entity_test.go
rename to pkg/middleware/entity_test.go
index b368293..42f4f33 100644
--- a/middleware/entity_test.go
+++ b/pkg/middleware/entity_test.go
@@ -4,9 +4,9 @@ import (
"fmt"
"testing"
- "goweb/context"
- "goweb/ent"
- "goweb/tests"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/pkg/middleware/log.go b/pkg/middleware/log.go
new file mode 100644
index 0000000..d9eede0
--- /dev/null
+++ b/pkg/middleware/log.go
@@ -0,0 +1,73 @@
+package middleware
+
+import (
+ "fmt"
+ "strconv"
+ "time"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/log"
+)
+
+// SetLogger initializes a logger for the current request and stores it in the context.
+// It's recommended to have this executed after Echo's RequestID() middleware because it will add
+// the request ID to the logger so that all log messages produced from this request have the
+// request ID in it. You can modify this code to include any other fields that you want to always
+// appear.
+func SetLogger() echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ // Include the request ID in the logger
+ rID := ctx.Response().Header().Get(echo.HeaderXRequestID)
+ logger := log.Ctx(ctx).With("request_id", rID)
+
+ // TODO include other fields you may want in all logs for this request
+ log.Set(ctx, logger)
+ return next(ctx)
+ }
+ }
+}
+
+// LogRequest logs the current request
+// Echo provides middleware similar to this, but we want to use our own logger
+func LogRequest() echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) (err error) {
+ req := ctx.Request()
+ res := ctx.Response()
+
+ // Track how long the request takes to complete
+ start := time.Now()
+ if err = next(ctx); err != nil {
+ ctx.Error(err)
+ }
+ stop := time.Now()
+
+ sub := log.Ctx(ctx).With(
+ "ip", ctx.RealIP(),
+ "host", req.Host,
+ "referer", req.Referer(),
+ "status", res.Status,
+ "bytes_in", func() string {
+ cl := req.Header.Get(echo.HeaderContentLength)
+ if cl == "" {
+ cl = "0"
+ }
+ return cl
+ }(),
+ "bytes_out", strconv.FormatInt(res.Size, 10),
+ "latency", stop.Sub(start).String(),
+ )
+
+ msg := fmt.Sprintf("%s %s", req.Method, req.URL.RequestURI())
+
+ if res.Status >= 500 {
+ sub.Error(msg)
+ } else {
+ sub.Info(msg)
+ }
+
+ return nil
+ }
+ }
+}
diff --git a/pkg/middleware/log_test.go b/pkg/middleware/log_test.go
new file mode 100644
index 0000000..129376a
--- /dev/null
+++ b/pkg/middleware/log_test.go
@@ -0,0 +1,109 @@
+package middleware
+
+import (
+ "context"
+ "log/slog"
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ echomw "github.com/labstack/echo/v4/middleware"
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "github.com/mikestefanello/pagoda/pkg/tests"
+ "github.com/stretchr/testify/require"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type mockLogHandler struct {
+ msg string
+ level string
+ group string
+ attr []slog.Attr
+}
+
+func (m *mockLogHandler) Enabled(_ context.Context, l slog.Level) bool {
+ return true
+}
+
+func (m *mockLogHandler) Handle(_ context.Context, r slog.Record) error {
+ m.level = r.Level.String()
+ m.msg = r.Message
+ return nil
+}
+
+func (m *mockLogHandler) WithAttrs(as []slog.Attr) slog.Handler {
+ if m.attr == nil {
+ m.attr = make([]slog.Attr, 0)
+ }
+ m.attr = append(m.attr, as...)
+ return m
+}
+
+func (m *mockLogHandler) WithGroup(name string) slog.Handler {
+ m.group = name
+ return m
+}
+
+func (m *mockLogHandler) GetAttr(key string) string {
+ if m.attr == nil {
+ return ""
+ }
+ for _, attr := range m.attr {
+ if attr.Key == key {
+ return attr.Value.String()
+ }
+ }
+
+ return ""
+}
+
+func TestLogRequestID(t *testing.T) {
+ ctx, _ := tests.NewContext(c.Web, "/")
+
+ h := new(mockLogHandler)
+ logger := slog.New(h)
+ log.Set(ctx, logger)
+
+ require.NoError(t, tests.ExecuteMiddleware(ctx, echomw.RequestID()))
+ require.NoError(t, tests.ExecuteMiddleware(ctx, SetLogger()))
+
+ log.Ctx(ctx).Info("test")
+ rID := ctx.Response().Header().Get(echo.HeaderXRequestID)
+ assert.Equal(t, rID, h.GetAttr("request_id"))
+}
+
+func TestLogRequest(t *testing.T) {
+ statusCode := 200
+ h := new(mockLogHandler)
+
+ exec := func() {
+ ctx, _ := tests.NewContext(c.Web, "http://test.localhost/abc?d=1&e=2")
+ logger := slog.New(h).With("previous", "param")
+ log.Set(ctx, logger)
+ ctx.Request().Header.Set("Referer", "ref.com")
+ ctx.Request().Header.Set(echo.HeaderXRealIP, "21.12.12.21")
+
+ require.NoError(t, tests.ExecuteHandler(ctx, func(ctx echo.Context) error {
+ return ctx.String(statusCode, "hello")
+ },
+ SetLogger(),
+ LogRequest(),
+ ))
+ }
+
+ exec()
+ assert.Equal(t, "param", h.GetAttr("previous"))
+ assert.Equal(t, "21.12.12.21", h.GetAttr("ip"))
+ assert.Equal(t, "test.localhost", h.GetAttr("host"))
+ assert.Equal(t, "ref.com", h.GetAttr("referer"))
+ assert.Equal(t, "200", h.GetAttr("status"))
+ assert.Equal(t, "0", h.GetAttr("bytes_in"))
+ assert.Equal(t, "5", h.GetAttr("bytes_out"))
+ assert.NotEmpty(t, h.GetAttr("latency"))
+ assert.Equal(t, "INFO", h.level)
+ assert.Equal(t, "GET /abc?d=1&e=2", h.msg)
+
+ statusCode = 500
+ exec()
+ assert.Equal(t, "ERROR", h.level)
+}
diff --git a/middleware/middleware_test.go b/pkg/middleware/middleware_test.go
similarity index 61%
rename from middleware/middleware_test.go
rename to pkg/middleware/middleware_test.go
index 2aa60c8..6c5d5c3 100644
--- a/middleware/middleware_test.go
+++ b/pkg/middleware/middleware_test.go
@@ -4,10 +4,10 @@ import (
"os"
"testing"
- "goweb/config"
- "goweb/ent"
- "goweb/services"
- "goweb/tests"
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/pkg/services"
+ "github.com/mikestefanello/pagoda/pkg/tests"
)
var (
@@ -21,11 +21,6 @@ func TestMain(m *testing.M) {
// Create a new container
c = services.NewContainer()
- defer func() {
- if err := c.Shutdown(); err != nil {
- c.Web.Logger.Fatal(err)
- }
- }()
// Create a user
var err error
@@ -35,5 +30,11 @@ func TestMain(m *testing.M) {
// Run tests
exitVal := m.Run()
+
+ // Shutdown the container
+ if err = c.Shutdown(); err != nil {
+ panic(err)
+ }
+
os.Exit(exitVal)
}
diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go
new file mode 100644
index 0000000..2e6a43c
--- /dev/null
+++ b/pkg/middleware/session.go
@@ -0,0 +1,19 @@
+package middleware
+
+import (
+ "github.com/gorilla/context"
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/session"
+)
+
+// Session sets the session storage in the request context
+func Session(store sessions.Store) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ defer context.Clear(ctx.Request())
+ session.Store(ctx, store)
+ return next(ctx)
+ }
+ }
+}
diff --git a/pkg/middleware/session_test.go b/pkg/middleware/session_test.go
new file mode 100644
index 0000000..b48181c
--- /dev/null
+++ b/pkg/middleware/session_test.go
@@ -0,0 +1,24 @@
+package middleware
+
+import (
+ "testing"
+
+ "github.com/gorilla/sessions"
+ "github.com/mikestefanello/pagoda/pkg/session"
+ "github.com/mikestefanello/pagoda/pkg/tests"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSession(t *testing.T) {
+ ctx, _ := tests.NewContext(c.Web, "/")
+ _, err := session.Get(ctx, "test")
+ assert.Equal(t, session.ErrStoreNotFound, err)
+
+ store := sessions.NewCookieStore([]byte("secret"))
+ err = tests.ExecuteMiddleware(ctx, Session(store))
+ require.NoError(t, err)
+
+ _, err = session.Get(ctx, "test")
+ assert.NotEqual(t, session.ErrStoreNotFound, err)
+}
diff --git a/msg/msg.go b/pkg/msg/msg.go
similarity index 54%
rename from msg/msg.go
rename to pkg/msg/msg.go
index 65626c2..86dcbe6 100644
--- a/msg/msg.go
+++ b/pkg/msg/msg.go
@@ -2,46 +2,55 @@ package msg
import (
"github.com/gorilla/sessions"
- "github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "github.com/mikestefanello/pagoda/pkg/session"
)
+// Type is a message type.
type Type string
const (
+ // TypeSuccess represents a success message type.
TypeSuccess Type = "success"
- TypeInfo Type = "info"
+
+ // TypeInfo represents a info message type.
+ TypeInfo Type = "info"
+
+ // TypeWarning represents a warning message type.
TypeWarning Type = "warning"
- TypeDanger Type = "danger"
+
+ // TypeError represents an error message type.
+ TypeError Type = "error"
)
const (
- // sessionName stores the name of the session which contains flash messages
+ // sessionName stores the name of the session which contains flash messages.
sessionName = "msg"
)
-// Success sets a success flash message
+// Success sets a success flash message.
func Success(ctx echo.Context, message string) {
Set(ctx, TypeSuccess, message)
}
-// Info sets an info flash message
+// Info sets an info flash message.
func Info(ctx echo.Context, message string) {
Set(ctx, TypeInfo, message)
}
-// Warning sets a warning flash message
+// Warning sets a warning flash message.
func Warning(ctx echo.Context, message string) {
Set(ctx, TypeWarning, message)
}
-// Danger sets a danger flash message
-func Danger(ctx echo.Context, message string) {
- Set(ctx, TypeDanger, message)
+// Error sets an error flash message.
+func Error(ctx echo.Context, message string) {
+ Set(ctx, TypeError, message)
}
-// Set adds a new flash message of a given type into the session storage
-// Errors will logged and not returned
+// Set adds a new flash message of a given type into the session storage.
+// Errors will be logged and not returned.
func Set(ctx echo.Context, typ Type, message string) {
if sess, err := getSession(ctx); err == nil {
sess.AddFlash(message, string(typ))
@@ -49,36 +58,40 @@ func Set(ctx echo.Context, typ Type, message string) {
}
}
-// Get gets flash messages of a given type from the session storage
-// Errors will logged and not returned
+// Get gets flash messages of a given type from the session storage.
+// Errors will be logged and not returned.
func Get(ctx echo.Context, typ Type) []string {
- var msgs []string
-
if sess, err := getSession(ctx); err == nil {
if flash := sess.Flashes(string(typ)); len(flash) > 0 {
save(ctx, sess)
+ msgs := make([]string, 0, len(flash))
for _, m := range flash {
msgs = append(msgs, m.(string))
}
+ return msgs
}
}
- return msgs
+ return nil
}
-// getSession gets the flash message session
+// getSession gets the flash message session.
func getSession(ctx echo.Context) (*sessions.Session, error) {
- sess, err := session.Get(sessionName, ctx)
+ sess, err := session.Get(ctx, sessionName)
if err != nil {
- ctx.Logger().Errorf("cannot load flash message session: %v", err)
+ log.Ctx(ctx).Error("cannot load flash message session",
+ "error", err,
+ )
}
return sess, err
}
-// save saves the flash message session
+// save saves the flash message session.
func save(ctx echo.Context, sess *sessions.Session) {
if err := sess.Save(ctx.Request(), ctx.Response()); err != nil {
- ctx.Logger().Errorf("failed to set flash message: %v", err)
+ log.Ctx(ctx).Error("failed to set flash message",
+ "error", err,
+ )
}
}
diff --git a/msg/msg_test.go b/pkg/msg/msg_test.go
similarity index 83%
rename from msg/msg_test.go
rename to pkg/msg/msg_test.go
index 9570885..8131fdb 100644
--- a/msg/msg_test.go
+++ b/pkg/msg/msg_test.go
@@ -3,9 +3,9 @@ package msg
import (
"testing"
- "goweb/tests"
+ "github.com/mikestefanello/pagoda/pkg/tests"
- "github.com/go-playground/assert/v2"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/labstack/echo/v4"
@@ -33,8 +33,8 @@ func TestMsg(t *testing.T) {
assertMsg(TypeInfo, text)
text = "ccc"
- Danger(ctx, text)
- assertMsg(TypeDanger, text)
+ Error(ctx, text)
+ assertMsg(TypeError, text)
text = "ddd"
Warning(ctx, text)
diff --git a/controller/pager.go b/pkg/pager/pager.go
similarity index 60%
rename from controller/pager.go
rename to pkg/pager/pager.go
index 39710d2..ae4672e 100644
--- a/controller/pager.go
+++ b/pkg/pager/pager.go
@@ -1,4 +1,4 @@
-package controller
+package pager
import (
"math"
@@ -7,37 +7,33 @@ import (
"github.com/labstack/echo/v4"
)
-const (
- // DefaultItemsPerPage stores the default amount of items per page
- DefaultItemsPerPage = 20
+// QueryKey stores the query key used to indicate the current page.
+const QueryKey = "page"
- // PageQueryKey stores the query key used to indicate the current page
- PageQueryKey = "page"
-)
-
-// Pager provides a mechanism to allow a user to page results via a query parameter
+// Pager provides a mechanism to allow a user to page results via a query parameter.
type Pager struct {
- // Items stores the total amount of items in the result set
+ // Items stores the total amount of items in the result set.
Items int
- // Page stores the current page number
+ // Page stores the current page number.
Page int
- // ItemsPerPage stores the amount of items to display per page
+ // ItemsPerPage stores the amount of items to display per page.
ItemsPerPage int
- // Pages stores the total amount of pages in the result set
+ // Pages stores the total amount of pages in the result set.
Pages int
}
-// NewPager creates a new Pager
+// NewPager creates a new Pager.
func NewPager(ctx echo.Context, itemsPerPage int) Pager {
p := Pager{
ItemsPerPage: itemsPerPage,
+ Pages: 1,
Page: 1,
}
- if page := ctx.QueryParam(PageQueryKey); page != "" {
+ if page := ctx.QueryParam(QueryKey); page != "" {
if pageInt, err := strconv.Atoi(page); err == nil {
if pageInt > 0 {
p.Page = pageInt
@@ -53,7 +49,12 @@ func NewPager(ctx echo.Context, itemsPerPage int) Pager {
// This should be used rather than setting either items or pages directly.
func (p *Pager) SetItems(items int) {
p.Items = items
- p.Pages = int(math.Ceil(float64(items) / float64(p.ItemsPerPage)))
+
+ if items > 0 {
+ p.Pages = int(math.Ceil(float64(items) / float64(p.ItemsPerPage)))
+ } else {
+ p.Pages = 1
+ }
if p.Page > p.Pages {
p.Page = p.Pages
@@ -61,18 +62,18 @@ func (p *Pager) SetItems(items int) {
}
// IsBeginning determines if the pager is at the beginning of the pages
-func (p Pager) IsBeginning() bool {
+func (p *Pager) IsBeginning() bool {
return p.Page == 1
}
// IsEnd determines if the pager is at the end of the pages
-func (p Pager) IsEnd() bool {
+func (p *Pager) IsEnd() bool {
return p.Page >= p.Pages
}
// GetOffset determines the offset of the results in order to get the items for
// the current page
-func (p Pager) GetOffset() int {
+func (p *Pager) GetOffset() int {
if p.Page == 0 {
p.Page = 1
}
diff --git a/controller/pager_test.go b/pkg/pager/pager_test.go
similarity index 65%
rename from controller/pager_test.go
rename to pkg/pager/pager_test.go
index f7fb89d..44cace9 100644
--- a/controller/pager_test.go
+++ b/pkg/pager/pager_test.go
@@ -1,41 +1,48 @@
-package controller
+package pager
import (
"fmt"
"testing"
- "goweb/tests"
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/tests"
"github.com/stretchr/testify/assert"
)
func TestNewPager(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/")
+ e := echo.New()
+ ctx, _ := tests.NewContext(e, "/")
pgr := NewPager(ctx, 10)
assert.Equal(t, 10, pgr.ItemsPerPage)
assert.Equal(t, 1, pgr.Page)
assert.Equal(t, 0, pgr.Items)
- assert.Equal(t, 0, pgr.Pages)
+ assert.Equal(t, 1, pgr.Pages)
- ctx, _ = tests.NewContext(c.Web, fmt.Sprintf("/abc?%s=%d", PageQueryKey, 2))
+ ctx, _ = tests.NewContext(e, fmt.Sprintf("/abc?%s=%d", QueryKey, 2))
pgr = NewPager(ctx, 10)
assert.Equal(t, 2, pgr.Page)
- ctx, _ = tests.NewContext(c.Web, fmt.Sprintf("/abc?%s=%d", PageQueryKey, -2))
+ ctx, _ = tests.NewContext(e, fmt.Sprintf("/abc?%s=%d", QueryKey, -2))
pgr = NewPager(ctx, 10)
assert.Equal(t, 1, pgr.Page)
}
func TestPager_SetItems(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/")
+ ctx, _ := tests.NewContext(echo.New(), "/")
pgr := NewPager(ctx, 20)
pgr.SetItems(100)
assert.Equal(t, 100, pgr.Items)
assert.Equal(t, 5, pgr.Pages)
+
+ pgr.SetItems(0)
+ assert.Equal(t, 0, pgr.Items)
+ assert.Equal(t, 1, pgr.Pages)
+ assert.Equal(t, 1, pgr.Page)
}
func TestPager_IsBeginning(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/")
+ ctx, _ := tests.NewContext(echo.New(), "/")
pgr := NewPager(ctx, 20)
pgr.Pages = 10
assert.True(t, pgr.IsBeginning())
@@ -46,7 +53,7 @@ func TestPager_IsBeginning(t *testing.T) {
}
func TestPager_IsEnd(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/")
+ ctx, _ := tests.NewContext(echo.New(), "/")
pgr := NewPager(ctx, 20)
pgr.Pages = 10
assert.False(t, pgr.IsEnd())
@@ -57,7 +64,7 @@ func TestPager_IsEnd(t *testing.T) {
}
func TestPager_GetOffset(t *testing.T) {
- ctx, _ := tests.NewContext(c.Web, "/")
+ ctx, _ := tests.NewContext(echo.New(), "/")
pgr := NewPager(ctx, 20)
assert.Equal(t, 0, pgr.GetOffset())
pgr.Page = 2
diff --git a/pkg/redirect/redirect.go b/pkg/redirect/redirect.go
new file mode 100644
index 0000000..73da260
--- /dev/null
+++ b/pkg/redirect/redirect.go
@@ -0,0 +1,91 @@
+package redirect
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/htmx"
+)
+
+// Redirect is a helper to perform HTTP redirects.
+type Redirect struct {
+ ctx echo.Context
+ url string
+ routeName string
+ routeParams []any
+ status int
+ query url.Values
+}
+
+// New initializes a new Redirect
+func New(ctx echo.Context) *Redirect {
+ return &Redirect{
+ ctx: ctx,
+ status: http.StatusTemporaryRedirect,
+ }
+}
+
+// Route sets the route name to redirect to.
+// Use either this or URL()
+func (r *Redirect) Route(name string) *Redirect {
+ r.routeName = name
+ return r
+}
+
+// Params sets the route params
+func (r *Redirect) Params(params ...any) *Redirect {
+ r.routeParams = params
+ return r
+}
+
+// StatusCode sets the HTTP status code which defaults to http.StatusTemporaryRedirect.
+// Does not apply to HTMX redirects.
+func (r *Redirect) StatusCode(code int) *Redirect {
+ r.status = code
+ return r
+}
+
+// Query sets a URL query
+func (r *Redirect) Query(query url.Values) *Redirect {
+ r.query = query
+ return r
+}
+
+// URL sets the URL to redirect to
+// Use either this or Route()
+func (r *Redirect) URL(url string) *Redirect {
+ r.url = url
+ return r
+}
+
+// Go performs the redirect
+// If the request is HTMX boosted, an HTMX redirect will be performed instead of an HTTP redirect
+func (r *Redirect) Go() error {
+ if r.routeName == "" && r.url == "" {
+ return errors.New("no redirect provided")
+ }
+
+ var dest string
+ if r.url != "" {
+ dest = r.url
+ } else {
+ dest = r.ctx.Echo().Reverse(r.routeName, r.routeParams...)
+ }
+
+ if len(r.query) > 0 {
+ dest = fmt.Sprintf("%s?%s", dest, r.query.Encode())
+ }
+
+ if htmx.GetRequest(r.ctx).Boosted {
+ htmx.Response{
+ Redirect: dest,
+ }.Apply(r.ctx)
+
+ return nil
+ } else {
+ return r.ctx.Redirect(r.status, dest)
+ }
+}
diff --git a/pkg/redirect/redirect_test.go b/pkg/redirect/redirect_test.go
new file mode 100644
index 0000000..4685f2a
--- /dev/null
+++ b/pkg/redirect/redirect_test.go
@@ -0,0 +1,77 @@
+package redirect
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/htmx"
+ "github.com/mikestefanello/pagoda/pkg/tests"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRedirect(t *testing.T) {
+ e := echo.New()
+ e.GET("/path/:first/and/:second", func(c echo.Context) error {
+ return nil
+ }).Name = "test"
+
+ redirect := func() (*Redirect, echo.Context) {
+ ctx, _ := tests.NewContext(e, "/")
+ return New(ctx), ctx
+ }
+
+ t.Run("route", func(t *testing.T) {
+ q := url.Values{}
+ q.Add("a", "1")
+ q.Add("b", "2")
+ r, ctx := redirect()
+ r.Route("test")
+ r.Params("one", "two")
+ r.Query(q)
+ r.StatusCode(http.StatusTemporaryRedirect)
+ require.NoError(t, r.Go())
+ assert.Equal(t, "/path/one/and/two?a=1&b=2", ctx.Response().Header().Get(echo.HeaderLocation))
+ assert.Equal(t, http.StatusTemporaryRedirect, ctx.Response().Status)
+ })
+
+ t.Run("route htmx", func(t *testing.T) {
+ q := url.Values{}
+ q.Add("a", "1")
+ q.Add("b", "2")
+ r, ctx := redirect()
+ ctx.Request().Header.Set(htmx.HeaderBoosted, "true")
+ r.Route("test")
+ r.Params("one", "two")
+ r.Query(q)
+ require.NoError(t, r.Go())
+ assert.Equal(t, "/path/one/and/two?a=1&b=2", ctx.Response().Header().Get(htmx.HeaderRedirect))
+ })
+
+ t.Run("url", func(t *testing.T) {
+ q := url.Values{}
+ q.Add("a", "1")
+ q.Add("b", "2")
+ r, ctx := redirect()
+ r.URL("https://localhost.dev")
+ r.Query(q)
+ r.StatusCode(http.StatusTemporaryRedirect)
+ require.NoError(t, r.Go())
+ assert.Equal(t, "https://localhost.dev?a=1&b=2", ctx.Response().Header().Get(echo.HeaderLocation))
+ assert.Equal(t, http.StatusTemporaryRedirect, ctx.Response().Status)
+ })
+
+ t.Run("url htmx", func(t *testing.T) {
+ q := url.Values{}
+ q.Add("a", "1")
+ q.Add("b", "2")
+ r, ctx := redirect()
+ ctx.Request().Header.Set(htmx.HeaderBoosted, "true")
+ r.URL("https://localhost.dev")
+ r.Query(q)
+ require.NoError(t, r.Go())
+ assert.Equal(t, "https://localhost.dev?a=1&b=2", ctx.Response().Header().Get(htmx.HeaderRedirect))
+ })
+}
diff --git a/pkg/routenames/names.go b/pkg/routenames/names.go
new file mode 100644
index 0000000..ee6c9a7
--- /dev/null
+++ b/pkg/routenames/names.go
@@ -0,0 +1,58 @@
+package routenames
+
+import (
+ "fmt"
+)
+
+const (
+ Home = "home"
+ About = "about"
+ Contact = "contact"
+ ContactSubmit = "contact.submit"
+ Login = "login"
+ LoginSubmit = "login.submit"
+ Register = "register"
+ RegisterSubmit = "register.submit"
+ ForgotPassword = "forgot_password"
+ ForgotPasswordSubmit = "forgot_password.submit"
+ Logout = "logout"
+ VerifyEmail = "verify_email"
+ ResetPassword = "reset_password"
+ ResetPasswordSubmit = "reset_password.submit"
+ Search = "search"
+ Task = "task"
+ TaskSubmit = "task.submit"
+ Cache = "cache"
+ CacheSubmit = "cache.submit"
+ Files = "files"
+ FilesSubmit = "files.submit"
+ AdminTasks = "admin:tasks"
+)
+
+func AdminEntityList(entityTypeName string) string {
+ return fmt.Sprintf("admin:%s_list", entityTypeName)
+}
+
+func AdminEntityAdd(entityTypeName string) string {
+ return fmt.Sprintf("admin:%s_add", entityTypeName)
+}
+
+func AdminEntityEdit(entityTypeName string) string {
+ return fmt.Sprintf("admin:%s_edit", entityTypeName)
+}
+
+func AdminEntityDelete(entityTypeName string) string {
+ return fmt.Sprintf("admin:%s_delete", entityTypeName)
+}
+
+func AdminEntityAddSubmit(entityTypeName string) string {
+ return fmt.Sprintf("admin:%s_add.submit", entityTypeName)
+}
+
+func AdminEntityEditSubmit(entityTypeName string) string {
+ return fmt.Sprintf("admin:%s_edit.submit", entityTypeName)
+}
+
+func AdminEntityDeleteSubmit(entityTypeName string) string {
+ return fmt.Sprintf("admin:%s_delete.submit", entityTypeName)
+}
diff --git a/services/auth.go b/pkg/services/auth.go
similarity index 64%
rename from services/auth.go
rename to pkg/services/auth.go
index 330cacb..a4a66f4 100644
--- a/services/auth.go
+++ b/pkg/services/auth.go
@@ -3,14 +3,18 @@ package services
import (
"crypto/rand"
"encoding/hex"
+ "errors"
+ "fmt"
"time"
- "goweb/config"
- "goweb/ent"
- "goweb/ent/passwordtoken"
- "goweb/ent/user"
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/user"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/session"
- "github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
)
@@ -58,7 +62,7 @@ func NewAuthClient(cfg *config.Config, orm *ent.Client) *AuthClient {
// Login logs in a user of a given ID
func (c *AuthClient) Login(ctx echo.Context, userID int) error {
- sess, err := session.Get(authSessionName, ctx)
+ sess, err := session.Get(ctx, authSessionName)
if err != nil {
return err
}
@@ -69,7 +73,7 @@ func (c *AuthClient) Login(ctx echo.Context, userID int) error {
// Logout logs the requesting user out
func (c *AuthClient) Logout(ctx echo.Context) error {
- sess, err := session.Get(authSessionName, ctx)
+ sess, err := session.Get(ctx, authSessionName)
if err != nil {
return err
}
@@ -79,7 +83,7 @@ func (c *AuthClient) Logout(ctx echo.Context) error {
// GetAuthenticatedUserID returns the authenticated user's ID, if the user is logged in
func (c *AuthClient) GetAuthenticatedUserID(ctx echo.Context) (int, error) {
- sess, err := session.Get(authSessionName, ctx)
+ sess, err := session.Get(ctx, authSessionName)
if err != nil {
return 0, err
}
@@ -102,15 +106,6 @@ func (c *AuthClient) GetAuthenticatedUser(ctx echo.Context) (*ent.User, error) {
return nil, NotAuthenticatedError{}
}
-// HashPassword returns a hash of a given password
-func (c *AuthClient) HashPassword(password string) (string, error) {
- hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
- if err != nil {
- return "", err
- }
- return string(hash), nil
-}
-
// CheckPassword check if a given password matches a given hash
func (c *AuthClient) CheckPassword(password, hash string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
@@ -119,7 +114,7 @@ func (c *AuthClient) CheckPassword(password, hash string) error {
// GeneratePasswordResetToken generates a password reset token for a given user.
// For security purposes, the token itself is not stored in the database but rather
// a hash of the token, exactly how passwords are handled. This method returns both
-// the generated token as well as the token entity which only contains the hash.
+// the generated token and the token entity which only contains the hash.
func (c *AuthClient) GeneratePasswordResetToken(ctx echo.Context, userID int) (string, *ent.PasswordToken, error) {
// Generate the token, which is what will go in the URL, but not the database
token, err := c.RandomToken(c.config.App.PasswordToken.Length)
@@ -127,48 +122,42 @@ func (c *AuthClient) GeneratePasswordResetToken(ctx echo.Context, userID int) (s
return "", nil, err
}
- // Hash the token, which is what will be stored in the database
- hash, err := c.HashPassword(token)
- if err != nil {
- return "", nil, err
- }
-
// Create and save the password reset token
pt, err := c.orm.PasswordToken.
Create().
- SetHash(hash).
+ SetToken(token).
SetUserID(userID).
Save(ctx.Request().Context())
return token, pt, err
}
-// GetValidPasswordToken returns a valid password token entity for a given user and a given token.
-// Since the actual token is not stored in the database for security purposes, all non-expired token entities
-// are fetched from the database belonging to the requesting user and a hash of the provided token is compared
-// with the hash stored in the database.
-func (c *AuthClient) GetValidPasswordToken(ctx echo.Context, token string, userID int) (*ent.PasswordToken, error) {
+// GetValidPasswordToken returns a valid, non-expired password token entity for a given user, token ID and token.
+// Since the actual token is not stored in the database for security purposes, if a matching password token entity is
+// found a hash of the provided token is compared with the hash stored in the database in order to validate.
+func (c *AuthClient) GetValidPasswordToken(ctx echo.Context, userID, tokenID int, token string) (*ent.PasswordToken, error) {
// Ensure expired tokens are never returned
expiration := time.Now().Add(-c.config.App.PasswordToken.Expiration)
- // Query to find all tokens for te user that haven't expired
- // We need to get all of them in order to properly match the token to the hashes
- pts, err := c.orm.PasswordToken.
+ // Query to find a password token entity that matches the given user and token ID
+ pt, err := c.orm.PasswordToken.
Query().
+ Where(passwordtoken.ID(tokenID)).
Where(passwordtoken.HasUserWith(user.ID(userID))).
Where(passwordtoken.CreatedAtGTE(expiration)).
- All(ctx.Request().Context())
+ Only(ctx.Request().Context())
- if err != nil {
- ctx.Logger().Error(err)
- return nil, err
- }
-
- // Check all tokens for a hash match
- for _, pt := range pts {
- if err := c.CheckPassword(token, pt.Hash); err == nil {
+ switch err.(type) {
+ case *ent.NotFoundError:
+ case nil:
+ // Check the token for a hash match
+ if err := c.CheckPassword(token, pt.Token); err == nil {
return pt, nil
}
+ default:
+ if !context.IsCanceledError(err) {
+ return nil, err
+ }
}
return nil, InvalidPasswordTokenError{}
@@ -194,3 +183,36 @@ func (c *AuthClient) RandomToken(length int) (string, error) {
token := hex.EncodeToString(b)
return token[:length], nil
}
+
+// GenerateEmailVerificationToken generates an email verification token for a given email address using JWT which
+// is set to expire based on the duration stored in configuration
+func (c *AuthClient) GenerateEmailVerificationToken(email string) (string, error) {
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "email": email,
+ "exp": time.Now().Add(c.config.App.EmailVerificationTokenExpiration).Unix(),
+ })
+
+ return token.SignedString([]byte(c.config.App.EncryptionKey))
+}
+
+// ValidateEmailVerificationToken validates an email verification token and returns the associated email address if
+// the token is valid and has not expired
+func (c *AuthClient) ValidateEmailVerificationToken(token string) (string, error) {
+ t, err := jwt.Parse(token, func(t *jwt.Token) (any, error) {
+ if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
+ }
+
+ return []byte(c.config.App.EncryptionKey), nil
+ })
+
+ if err != nil {
+ return "", err
+ }
+
+ if claims, ok := t.Claims.(jwt.MapClaims); ok && t.Valid {
+ return claims["email"].(string), nil
+ }
+
+ return "", errors.New("invalid or expired token")
+}
diff --git a/services/auth_test.go b/pkg/services/auth_test.go
similarity index 52%
rename from services/auth_test.go
rename to pkg/services/auth_test.go
index bc335c4..459446e 100644
--- a/services/auth_test.go
+++ b/pkg/services/auth_test.go
@@ -6,15 +6,16 @@ import (
"testing"
"time"
- "goweb/ent/passwordtoken"
- "goweb/ent/user"
+ "github.com/mikestefanello/pagoda/ent/passwordtoken"
+ "github.com/mikestefanello/pagoda/ent/user"
+ "golang.org/x/crypto/bcrypt"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
)
-func TestAuth(t *testing.T) {
+func TestAuthClient_Auth(t *testing.T) {
assertNoAuth := func() {
_, err := c.Auth.GetAuthenticatedUserID(ctx)
assert.True(t, errors.Is(err, NotAuthenticatedError{}))
@@ -41,48 +42,49 @@ func TestAuth(t *testing.T) {
assertNoAuth()
}
-func TestPasswordHashing(t *testing.T) {
+func TestAuthClient_CheckPassword(t *testing.T) {
pw := "testcheckpassword"
- hash, err := c.Auth.HashPassword(pw)
- assert.NoError(t, err)
+ hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
+ require.NoError(t, err)
assert.NotEqual(t, hash, pw)
- err = c.Auth.CheckPassword(pw, hash)
+ err = c.Auth.CheckPassword(pw, string(hash))
assert.NoError(t, err)
}
-func TestGeneratePasswordResetToken(t *testing.T) {
+func TestAuthClient_GeneratePasswordResetToken(t *testing.T) {
token, pt, err := c.Auth.GeneratePasswordResetToken(ctx, usr.ID)
require.NoError(t, err)
assert.Len(t, token, c.Config.App.PasswordToken.Length)
- assert.NoError(t, c.Auth.CheckPassword(token, pt.Hash))
+ assert.NoError(t, c.Auth.CheckPassword(token, pt.Token))
}
-func TestGetValidPasswordToken(t *testing.T) {
+func TestAuthClient_GetValidPasswordToken(t *testing.T) {
// Check that a fake token is not valid
- _, err := c.Auth.GetValidPasswordToken(ctx, "faketoken", usr.ID)
+ _, err := c.Auth.GetValidPasswordToken(ctx, usr.ID, 1, "faketoken")
assert.Error(t, err)
// Generate a valid token and check that it is returned
token, pt, err := c.Auth.GeneratePasswordResetToken(ctx, usr.ID)
require.NoError(t, err)
- pt2, err := c.Auth.GetValidPasswordToken(ctx, token, usr.ID)
+ pt2, err := c.Auth.GetValidPasswordToken(ctx, usr.ID, pt.ID, token)
require.NoError(t, err)
assert.Equal(t, pt.ID, pt2.ID)
- // Expire the token by pushed the date far enough back
- _, err = c.ORM.PasswordToken.
+ // Expire the token by pushing the date far enough back
+ count, err := c.ORM.PasswordToken.
Update().
- SetCreatedAt(time.Now().Add(-(c.Config.App.PasswordToken.Expiration + 10))).
+ SetCreatedAt(time.Now().Add(-(c.Config.App.PasswordToken.Expiration + time.Hour))).
Where(passwordtoken.ID(pt.ID)).
Save(context.Background())
require.NoError(t, err)
+ require.Equal(t, 1, count)
// Expired tokens should not be valid
- _, err = c.Auth.GetValidPasswordToken(ctx, token, usr.ID)
+ _, err = c.Auth.GetValidPasswordToken(ctx, usr.ID, pt.ID, token)
assert.Error(t, err)
}
-func TestDeletePasswordTokens(t *testing.T) {
+func TestAuthClient_DeletePasswordTokens(t *testing.T) {
// Create three tokens for the user
for i := 0; i < 3; i++ {
_, _, err := c.Auth.GeneratePasswordResetToken(ctx, usr.ID)
@@ -103,7 +105,7 @@ func TestDeletePasswordTokens(t *testing.T) {
assert.Equal(t, 0, count)
}
-func TestRandomToken(t *testing.T) {
+func TestAuthClient_RandomToken(t *testing.T) {
length := c.Config.App.PasswordToken.Length
a, err := c.Auth.RandomToken(length)
require.NoError(t, err)
@@ -113,3 +115,33 @@ func TestRandomToken(t *testing.T) {
assert.Len(t, b, length)
assert.NotEqual(t, a, b)
}
+
+func TestAuthClient_EmailVerificationToken(t *testing.T) {
+ t.Run("valid token", func(t *testing.T) {
+ email := "test@localhost.com"
+ token, err := c.Auth.GenerateEmailVerificationToken(email)
+ require.NoError(t, err)
+
+ tokenEmail, err := c.Auth.ValidateEmailVerificationToken(token)
+ require.NoError(t, err)
+ assert.Equal(t, email, tokenEmail)
+ })
+
+ t.Run("invalid token", func(t *testing.T) {
+ badToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAbG9jYWxob3N0LmNvbSIsImV4cCI6MTkxNzg2NDAwMH0.ScJCpfEEzlilKfRs_aVouzwPNKI28M3AIm-hyImQHUQ"
+ _, err := c.Auth.ValidateEmailVerificationToken(badToken)
+ assert.Error(t, err)
+ })
+
+ t.Run("expired token", func(t *testing.T) {
+ c.Config.App.EmailVerificationTokenExpiration = -time.Hour
+ email := "test@localhost.com"
+ token, err := c.Auth.GenerateEmailVerificationToken(email)
+ require.NoError(t, err)
+
+ _, err = c.Auth.ValidateEmailVerificationToken(token)
+ assert.Error(t, err)
+
+ c.Config.App.EmailVerificationTokenExpiration = time.Hour * 12
+ })
+}
diff --git a/pkg/services/cache.go b/pkg/services/cache.go
new file mode 100644
index 0000000..331ef75
--- /dev/null
+++ b/pkg/services/cache.go
@@ -0,0 +1,351 @@
+package services
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/maypok86/otter"
+)
+
+// ErrCacheMiss indicates that the requested key does not exist in the cache
+var ErrCacheMiss = errors.New("cache miss")
+
+type (
+ // CacheStore provides an interface for cache storage
+ CacheStore interface {
+ // get attempts to get a cached value
+ get(context.Context, *CacheGetOp) (any, error)
+
+ // set attempts to set an entry in the cache
+ set(context.Context, *CacheSetOp) error
+
+ // flush removes a given key and/or tags from the cache
+ flush(context.Context, *CacheFlushOp) error
+
+ // close shuts down the cache storage
+ close()
+ }
+
+ // CacheClient is the client that allows you to interact with the cache
+ CacheClient struct {
+ // store holds the Cache storage
+ store CacheStore
+ }
+
+ // CacheSetOp handles chaining a set operation
+ CacheSetOp struct {
+ client *CacheClient
+ key string
+ group string
+ data any
+ expiration time.Duration
+ tags []string
+ }
+
+ // CacheGetOp handles chaining a get operation
+ CacheGetOp struct {
+ client *CacheClient
+ key string
+ group string
+ }
+
+ // CacheFlushOp handles chaining a flush operation
+ CacheFlushOp struct {
+ client *CacheClient
+ key string
+ group string
+ tags []string
+ }
+
+ // inMemoryCacheStore is a cache store implementation in memory
+ inMemoryCacheStore struct {
+ store *otter.CacheWithVariableTTL[string, any]
+ tagIndex *tagIndex
+ }
+
+ // tagIndex maintains an index to support cache tags for in-memory cache stores.
+ // There is a performance and memory impact to using cache tags since set and get operations using tags will require
+ // locking, and we need to keep track of this index in order to keep everything in sync.
+ // If using something like Redis for caching, you can leverage sets to store the index.
+ // Cache tags can be useful and convenient, so you should decide if your app benefits enough from this.
+ // As it stands here, there is no limiting how much memory this will consume and it will track all keys
+ // and tags added and removed from the cache. You could store these in the cache itself but allowing these to
+ // be evicted poses challenges.
+ tagIndex struct {
+ sync.Mutex
+ tags map[string]map[string]struct{} // tag->keys
+ keys map[string]map[string]struct{} // key->tags
+ }
+)
+
+// NewCacheClient creates a new cache client
+func NewCacheClient(store CacheStore) *CacheClient {
+ return &CacheClient{store: store}
+}
+
+// Close closes the connection to the cache
+func (c *CacheClient) Close() {
+ c.store.close()
+}
+
+// Set creates a cache set operation
+func (c *CacheClient) Set() *CacheSetOp {
+ return &CacheSetOp{
+ client: c,
+ }
+}
+
+// Get creates a cache get operation
+func (c *CacheClient) Get() *CacheGetOp {
+ return &CacheGetOp{
+ client: c,
+ }
+}
+
+// Flush creates a cache flush operation
+func (c *CacheClient) Flush() *CacheFlushOp {
+ return &CacheFlushOp{
+ client: c,
+ }
+}
+
+// cacheKey formats a cache key with an optional group
+func (c *CacheClient) cacheKey(group, key string) string {
+ if group != "" {
+ return fmt.Sprintf("%s::%s", group, key)
+ }
+ return key
+}
+
+// Key sets the cache key
+func (c *CacheSetOp) Key(key string) *CacheSetOp {
+ c.key = key
+ return c
+}
+
+// Group sets the cache group
+func (c *CacheSetOp) Group(group string) *CacheSetOp {
+ c.group = group
+ return c
+}
+
+// Data sets the data to cache
+func (c *CacheSetOp) Data(data any) *CacheSetOp {
+ c.data = data
+ return c
+}
+
+// Expiration sets the expiration duration of the cached data
+func (c *CacheSetOp) Expiration(expiration time.Duration) *CacheSetOp {
+ c.expiration = expiration
+ return c
+}
+
+// Tags sets the cache tags
+func (c *CacheSetOp) Tags(tags ...string) *CacheSetOp {
+ c.tags = tags
+ return c
+}
+
+// Save saves the data in the cache
+func (c *CacheSetOp) Save(ctx context.Context) error {
+ switch {
+ case c.key == "":
+ return errors.New("no cache key specified")
+ case c.data == nil:
+ return errors.New("no cache data specified")
+ case c.expiration == 0:
+ return errors.New("no cache expiration specified")
+ }
+
+ return c.client.store.set(ctx, c)
+}
+
+// Key sets the cache key
+func (c *CacheGetOp) Key(key string) *CacheGetOp {
+ c.key = key
+ return c
+}
+
+// Group sets the cache group
+func (c *CacheGetOp) Group(group string) *CacheGetOp {
+ c.group = group
+ return c
+}
+
+// Fetch fetches the data from the cache
+func (c *CacheGetOp) Fetch(ctx context.Context) (any, error) {
+ if c.key == "" {
+ return nil, errors.New("no cache key specified")
+ }
+
+ return c.client.store.get(ctx, c)
+}
+
+// Key sets the cache key
+func (c *CacheFlushOp) Key(key string) *CacheFlushOp {
+ c.key = key
+ return c
+}
+
+// Group sets the cache group
+func (c *CacheFlushOp) Group(group string) *CacheFlushOp {
+ c.group = group
+ return c
+}
+
+// Tags sets the cache tags
+func (c *CacheFlushOp) Tags(tags ...string) *CacheFlushOp {
+ c.tags = tags
+ return c
+}
+
+// Execute flushes the data from the cache
+func (c *CacheFlushOp) Execute(ctx context.Context) error {
+ return c.client.store.flush(ctx, c)
+}
+
+// newInMemoryCache creates a new in-memory CacheStore
+func newInMemoryCache(capacity int) (CacheStore, error) {
+ s := &inMemoryCacheStore{
+ tagIndex: newTagIndex(),
+ }
+
+ store, err := otter.MustBuilder[string, any](capacity).
+ WithVariableTTL().
+ DeletionListener(func(key string, value any, cause otter.DeletionCause) {
+ s.tagIndex.purgeKeys(key)
+ }).
+ Build()
+
+ if err != nil {
+ return nil, err
+ }
+
+ s.store = &store
+
+ return s, nil
+}
+
+func (s *inMemoryCacheStore) get(_ context.Context, op *CacheGetOp) (any, error) {
+ v, exists := s.store.Get(op.client.cacheKey(op.group, op.key))
+
+ if !exists {
+ return nil, ErrCacheMiss
+ }
+
+ return v, nil
+}
+
+func (s *inMemoryCacheStore) set(_ context.Context, op *CacheSetOp) error {
+ key := op.client.cacheKey(op.group, op.key)
+
+ added := s.store.Set(
+ key,
+ op.data,
+ op.expiration,
+ )
+
+ if len(op.tags) > 0 {
+ s.tagIndex.setTags(key, op.tags...)
+ }
+
+ if !added {
+ return errors.New("cache set failed")
+ }
+
+ return nil
+}
+
+func (s *inMemoryCacheStore) flush(_ context.Context, op *CacheFlushOp) error {
+ keys := make([]string, 0)
+
+ if key := op.client.cacheKey(op.group, op.key); key != "" {
+ keys = append(keys, key)
+ }
+
+ if len(op.tags) > 0 {
+ keys = append(keys, s.tagIndex.purgeTags(op.tags...)...)
+ }
+
+ for _, key := range keys {
+ s.store.Delete(key)
+ }
+
+ s.tagIndex.purgeKeys(keys...)
+
+ return nil
+}
+
+func (s *inMemoryCacheStore) close() {
+ s.store.Close()
+}
+
+func newTagIndex() *tagIndex {
+ return &tagIndex{
+ tags: make(map[string]map[string]struct{}),
+ keys: make(map[string]map[string]struct{}),
+ }
+}
+
+func (i *tagIndex) setTags(key string, tags ...string) {
+ i.Lock()
+ defer i.Unlock()
+
+ if _, exists := i.keys[key]; !exists {
+ i.keys[key] = make(map[string]struct{})
+ }
+
+ for _, tag := range tags {
+ if _, exists := i.tags[tag]; !exists {
+ i.tags[tag] = make(map[string]struct{})
+ }
+ i.tags[tag][key] = struct{}{}
+ i.keys[key][tag] = struct{}{}
+ }
+}
+
+func (i *tagIndex) purgeTags(tags ...string) []string {
+ i.Lock()
+ defer i.Unlock()
+
+ keys := make([]string, 0)
+
+ for _, tag := range tags {
+ if tagKeys, exists := i.tags[tag]; exists {
+ delete(i.tags, tag)
+
+ for key := range tagKeys {
+ delete(i.keys[key], tag)
+ if len(i.keys[key]) == 0 {
+ delete(i.keys, key)
+ }
+
+ keys = append(keys, key)
+ }
+ }
+ }
+
+ return keys
+}
+
+func (i *tagIndex) purgeKeys(keys ...string) {
+ i.Lock()
+ defer i.Unlock()
+
+ for _, key := range keys {
+ if keyTags, exists := i.keys[key]; exists {
+ delete(i.keys, key)
+
+ for tag := range keyTags {
+ delete(i.tags[tag], key)
+ if len(i.tags[tag]) == 0 {
+ delete(i.tags, tag)
+ }
+ }
+ }
+ }
+}
diff --git a/pkg/services/cache_test.go b/pkg/services/cache_test.go
new file mode 100644
index 0000000..fdaa308
--- /dev/null
+++ b/pkg/services/cache_test.go
@@ -0,0 +1,105 @@
+package services
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCacheClient(t *testing.T) {
+ type cacheTest struct {
+ Value string
+ }
+
+ // Cache some data
+ data := cacheTest{Value: "abcdef"}
+ group := "testgroup"
+ key := "testkey"
+ err := c.Cache.
+ Set().
+ Group(group).
+ Key(key).
+ Data(data).
+ Expiration(500 * time.Millisecond).
+ Save(context.Background())
+ require.NoError(t, err)
+
+ // Get the data
+ fromCache, err := c.Cache.
+ Get().
+ Group(group).
+ Key(key).
+ Fetch(context.Background())
+ require.NoError(t, err)
+ cast, ok := fromCache.(cacheTest)
+ require.True(t, ok)
+ assert.Equal(t, data, cast)
+
+ // The same key with the wrong group should fail
+ _, err = c.Cache.
+ Get().
+ Key(key).
+ Fetch(context.Background())
+ assert.Equal(t, ErrCacheMiss, err)
+
+ // Flush the data
+ err = c.Cache.
+ Flush().
+ Group(group).
+ Key(key).
+ Execute(context.Background())
+ require.NoError(t, err)
+
+ // The data should be gone
+ assertFlushed := func(key string) {
+ // The data should be gone
+ _, err = c.Cache.
+ Get().
+ Group(group).
+ Key(key).
+ Fetch(context.Background())
+ assert.Equal(t, ErrCacheMiss, err)
+ }
+ assertFlushed(key)
+
+ // Set with tags
+ key = "testkey2"
+ err = c.Cache.
+ Set().
+ Group(group).
+ Key(key).
+ Data(data).
+ Tags("tag1", "tag2").
+ Expiration(time.Hour).
+ Save(context.Background())
+ require.NoError(t, err)
+
+ // Check the tag index
+ index := c.Cache.store.(*inMemoryCacheStore).tagIndex
+ gk := c.Cache.cacheKey(group, key)
+ _, exists := index.tags["tag1"][gk]
+ assert.True(t, exists)
+ _, exists = index.tags["tag2"][gk]
+ assert.True(t, exists)
+ _, exists = index.keys[gk]["tag1"]
+ assert.True(t, exists)
+ _, exists = index.keys[gk]["tag2"]
+ assert.True(t, exists)
+
+ // Flush one of tags
+ err = c.Cache.
+ Flush().
+ Tags("tag1").
+ Execute(context.Background())
+ require.NoError(t, err)
+
+ // The data should be gone
+ assertFlushed(key)
+
+ // The index should be empty
+ assert.Empty(t, index.tags)
+ assert.Empty(t, index.keys)
+}
diff --git a/pkg/services/container.go b/pkg/services/container.go
new file mode 100644
index 0000000..2a2ab45
--- /dev/null
+++ b/pkg/services/container.go
@@ -0,0 +1,246 @@
+package services
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "log/slog"
+ "math/rand"
+ "os"
+ "strings"
+
+ entsql "entgo.io/ent/dialect/sql"
+ "github.com/labstack/echo/v4"
+ _ "github.com/mattn/go-sqlite3"
+ "github.com/mikestefanello/backlite"
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "github.com/spf13/afero"
+
+ // Required by ent.
+ _ "github.com/mikestefanello/pagoda/ent/runtime"
+)
+
+// Container contains all services used by the application and provides an easy way to handle dependency
+// injection including within tests.
+type Container struct {
+ // Validator stores a validator
+ Validator *Validator
+
+ // Web stores the web framework.
+ Web *echo.Echo
+
+ // Config stores the application configuration.
+ Config *config.Config
+
+ // Cache contains the cache client.
+ Cache *CacheClient
+
+ // Database stores the connection to the database.
+ Database *sql.DB
+
+ // Files stores the file system.
+ Files afero.Fs
+
+ // ORM stores a client to the ORM.
+ ORM *ent.Client
+
+ // Mail stores an email sending client.
+ Mail *MailClient
+
+ // Auth stores an authentication client.
+ Auth *AuthClient
+
+ // Tasks stores the task client.
+ Tasks *backlite.Client
+}
+
+// NewContainer creates and initializes a new Container.
+func NewContainer() *Container {
+ c := new(Container)
+ c.initConfig()
+ c.initValidator()
+ c.initWeb()
+ c.initCache()
+ c.initDatabase()
+ c.initFiles()
+ c.initORM()
+ c.initAuth()
+ c.initMail()
+ c.initTasks()
+ return c
+}
+
+// Shutdown gracefully shuts the Container down and disconnects all connections.
+func (c *Container) Shutdown() error {
+ // Shutdown the web server.
+ webCtx, webCancel := context.WithTimeout(context.Background(), c.Config.HTTP.ShutdownTimeout)
+ defer webCancel()
+ if err := c.Web.Shutdown(webCtx); err != nil {
+ return err
+ }
+
+ // Shutdown the task runner.
+ taskCtx, taskCancel := context.WithTimeout(context.Background(), c.Config.Tasks.ShutdownTimeout)
+ defer taskCancel()
+ c.Tasks.Stop(taskCtx)
+
+ // Shutdown the ORM.
+ if err := c.ORM.Close(); err != nil {
+ return err
+ }
+
+ // Shutdown the database.
+ if err := c.Database.Close(); err != nil {
+ return err
+ }
+
+ // Shutdown the cache.
+ c.Cache.Close()
+
+ return nil
+}
+
+// initConfig initializes configuration.
+func (c *Container) initConfig() {
+ cfg, err := config.GetConfig()
+ if err != nil {
+ panic(fmt.Sprintf("failed to load config: %v", err))
+ }
+ c.Config = &cfg
+
+ // Configure logging.
+ switch cfg.App.Environment {
+ case config.EnvProduction:
+ slog.SetLogLoggerLevel(slog.LevelInfo)
+ default:
+ slog.SetLogLoggerLevel(slog.LevelDebug)
+ }
+}
+
+// initValidator initializes the validator.
+func (c *Container) initValidator() {
+ c.Validator = NewValidator()
+}
+
+// initWeb initializes the web framework.
+func (c *Container) initWeb() {
+ c.Web = echo.New()
+ c.Web.HideBanner = true
+ c.Web.Validator = c.Validator
+}
+
+// initCache initializes the cache.
+func (c *Container) initCache() {
+ store, err := newInMemoryCache(c.Config.Cache.Capacity)
+ if err != nil {
+ panic(err)
+ }
+
+ c.Cache = NewCacheClient(store)
+}
+
+// initDatabase initializes the database.
+func (c *Container) initDatabase() {
+ var err error
+ var connection string
+
+ switch c.Config.App.Environment {
+ case config.EnvTest:
+ // TODO: Drop/recreate the DB, if this isn't in memory?
+ connection = c.Config.Database.TestConnection
+ default:
+ connection = c.Config.Database.Connection
+ }
+
+ c.Database, err = openDB(c.Config.Database.Driver, connection)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// initFiles initializes the file system.
+func (c *Container) initFiles() {
+ // Use in-memory storage for tests.
+ if c.Config.App.Environment == config.EnvTest {
+ c.Files = afero.NewMemMapFs()
+ return
+ }
+
+ fs := afero.NewOsFs()
+ if err := fs.MkdirAll(c.Config.Files.Directory, 0755); err != nil {
+ panic(err)
+ }
+ c.Files = afero.NewBasePathFs(fs, c.Config.Files.Directory)
+}
+
+// initORM initializes the ORM.
+func (c *Container) initORM() {
+ drv := entsql.OpenDB(c.Config.Database.Driver, c.Database)
+ c.ORM = ent.NewClient(ent.Driver(drv))
+
+ // Run the auto migration tool.
+ if err := c.ORM.Schema.Create(context.Background()); err != nil {
+ panic(err)
+ }
+}
+
+// initAuth initializes the authentication client.
+func (c *Container) initAuth() {
+ c.Auth = NewAuthClient(c.Config, c.ORM)
+}
+
+// initMail initialize the mail client.
+func (c *Container) initMail() {
+ var err error
+ c.Mail, err = NewMailClient(c.Config)
+ if err != nil {
+ panic(fmt.Sprintf("failed to create mail client: %v", err))
+ }
+}
+
+// initTasks initializes the task client.
+func (c *Container) initTasks() {
+ var err error
+ // You could use a separate database for tasks, if you'd like, but using one
+ // makes transaction support easier.
+ c.Tasks, err = backlite.NewClient(backlite.ClientConfig{
+ DB: c.Database,
+ Logger: log.Default(),
+ NumWorkers: c.Config.Tasks.Goroutines,
+ ReleaseAfter: c.Config.Tasks.ReleaseAfter,
+ CleanupInterval: c.Config.Tasks.CleanupInterval,
+ })
+
+ if err != nil {
+ panic(fmt.Sprintf("failed to create task client: %v", err))
+ }
+
+ if err = c.Tasks.Install(); err != nil {
+ panic(fmt.Sprintf("failed to install task schema: %v", err))
+ }
+}
+
+// openDB opens a database connection.
+func openDB(driver, connection string) (*sql.DB, error) {
+ if driver == "sqlite3" {
+ // Helper to automatically create the directories that the specified sqlite file
+ // should reside in, if one.
+ d := strings.Split(connection, "/")
+ if len(d) > 1 {
+ dirpath := strings.Join(d[:len(d)-1], "/")
+
+ if err := os.MkdirAll(dirpath, 0755); err != nil {
+ return nil, err
+ }
+ }
+
+ // Check if a random value is required, which is often used for in-memory test databases.
+ if strings.Contains(connection, "$RAND") {
+ connection = strings.Replace(connection, "$RAND", fmt.Sprint(rand.Int()), 1)
+ }
+ }
+
+ return sql.Open(driver, connection)
+}
diff --git a/services/container_test.go b/pkg/services/container_test.go
similarity index 86%
rename from services/container_test.go
rename to pkg/services/container_test.go
index dc875d9..7f4a5d4 100644
--- a/services/container_test.go
+++ b/pkg/services/container_test.go
@@ -12,8 +12,9 @@ func TestNewContainer(t *testing.T) {
assert.NotNil(t, c.Validator)
assert.NotNil(t, c.Cache)
assert.NotNil(t, c.Database)
+ assert.NotNil(t, c.Files)
assert.NotNil(t, c.ORM)
assert.NotNil(t, c.Mail)
assert.NotNil(t, c.Auth)
- assert.NotNil(t, c.TemplateRenderer)
+ assert.NotNil(t, c.Tasks)
}
diff --git a/pkg/services/mail.go b/pkg/services/mail.go
new file mode 100644
index 0000000..22e7d3d
--- /dev/null
+++ b/pkg/services/mail.go
@@ -0,0 +1,127 @@
+package services
+
+import (
+ "bytes"
+ "errors"
+
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "maragu.dev/gomponents"
+
+ "github.com/labstack/echo/v4"
+)
+
+type (
+ // MailClient provides a client for sending email
+ // This is purposely not completed because there are many different methods and services
+ // for sending email, many of which are very different. Choose what works best for you
+ // and populate the methods below. For now, emails will just be logged.
+ MailClient struct {
+ // config stores application configuration.
+ config *config.Config
+ }
+
+ // mail represents an email to be sent.
+ mail struct {
+ client *MailClient
+ from string
+ to string
+ subject string
+ body string
+ component gomponents.Node
+ }
+)
+
+// NewMailClient creates a new MailClient.
+func NewMailClient(cfg *config.Config) (*MailClient, error) {
+ return &MailClient{
+ config: cfg,
+ }, nil
+}
+
+// Compose creates a new email.
+func (m *MailClient) Compose() *mail {
+ return &mail{
+ client: m,
+ from: m.config.Mail.FromAddress,
+ }
+}
+
+// skipSend determines if mail sending should be skipped.
+func (m *MailClient) skipSend() bool {
+ return m.config.App.Environment != config.EnvProduction
+}
+
+// send attempts to send the email.
+func (m *MailClient) send(email *mail, ctx echo.Context) error {
+ switch {
+ case email.to == "":
+ return errors.New("email cannot be sent without a to address")
+ case email.body == "" && email.component == nil:
+ return errors.New("email cannot be sent without a body or component to render")
+ }
+
+ // Check if a component was supplied.
+ if email.component != nil {
+ // Render the component and use as the body.
+ // TODO pool the buffers?
+ buf := bytes.NewBuffer(nil)
+ if err := email.component.Render(buf); err != nil {
+ return err
+ }
+
+ email.body = buf.String()
+ }
+
+ // Check if mail sending should be skipped.
+ if m.skipSend() {
+ log.Ctx(ctx).Debug("skipping email delivery",
+ "to", email.to,
+ )
+ return nil
+ }
+
+ // TODO: Finish based on your mail sender of choice or stop logging below!
+ log.Ctx(ctx).Info("sending email",
+ "to", email.to,
+ "subject", email.subject,
+ "body", email.body,
+ )
+ return nil
+}
+
+// From sets the email from address.
+func (m *mail) From(from string) *mail {
+ m.from = from
+ return m
+}
+
+// To sets the email address this email will be sent to.
+func (m *mail) To(to string) *mail {
+ m.to = to
+ return m
+}
+
+// Subject sets the subject line of the email.
+func (m *mail) Subject(subject string) *mail {
+ m.subject = subject
+ return m
+}
+
+// Body sets the body of the email.
+// This is not required and will be ignored if a component is set via Component().
+func (m *mail) Body(body string) *mail {
+ m.body = body
+ return m
+}
+
+// Component sets a renderable component to use as the body of the email.
+func (m *mail) Component(component gomponents.Node) *mail {
+ m.component = component
+ return m
+}
+
+// Send attempts to send the email.
+func (m *mail) Send(ctx echo.Context) error {
+ return m.client.send(m, ctx)
+}
diff --git a/services/mail_test.go b/pkg/services/mail_test.go
similarity index 100%
rename from services/mail_test.go
rename to pkg/services/mail_test.go
diff --git a/services/services_test.go b/pkg/services/services_test.go
similarity index 71%
rename from services/services_test.go
rename to pkg/services/services_test.go
index a67fb07..50b39fe 100644
--- a/services/services_test.go
+++ b/pkg/services/services_test.go
@@ -4,9 +4,9 @@ import (
"os"
"testing"
- "goweb/config"
- "goweb/ent"
- "goweb/tests"
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/pkg/tests"
"github.com/labstack/echo/v4"
)
@@ -23,11 +23,6 @@ func TestMain(m *testing.M) {
// Create a new container
c = NewContainer()
- defer func() {
- if err := c.Shutdown(); err != nil {
- c.Web.Logger.Fatal(err)
- }
- }()
// Create a web context
ctx, _ = tests.NewContext(c.Web, "/")
@@ -41,5 +36,11 @@ func TestMain(m *testing.M) {
// Run tests
exitVal := m.Run()
+
+ // Shutdown the container
+ if err = c.Shutdown(); err != nil {
+ panic(err)
+ }
+
os.Exit(exitVal)
}
diff --git a/services/validator.go b/pkg/services/validator.go
similarity index 90%
rename from services/validator.go
rename to pkg/services/validator.go
index 863976d..9f45efd 100644
--- a/services/validator.go
+++ b/pkg/services/validator.go
@@ -18,7 +18,7 @@ func NewValidator() *Validator {
}
// Validate validates a struct
-func (v *Validator) Validate(i interface{}) error {
+func (v *Validator) Validate(i any) error {
if err := v.validator.Struct(i); err != nil {
return err
}
diff --git a/services/validator_test.go b/pkg/services/validator_test.go
similarity index 100%
rename from services/validator_test.go
rename to pkg/services/validator_test.go
diff --git a/pkg/session/session.go b/pkg/session/session.go
new file mode 100644
index 0000000..56fe573
--- /dev/null
+++ b/pkg/session/session.go
@@ -0,0 +1,27 @@
+package session
+
+import (
+ "errors"
+
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/context"
+)
+
+// ErrStoreNotFound indicates that the session store was not present in the context
+var ErrStoreNotFound = errors.New("session store not found")
+
+// Get returns a session
+func Get(ctx echo.Context, name string) (*sessions.Session, error) {
+ s := ctx.Get(context.SessionKey)
+ if s == nil {
+ return nil, ErrStoreNotFound
+ }
+ store := s.(sessions.Store)
+ return store.Get(ctx.Request(), name)
+}
+
+// Store sets the session storage in the context
+func Store(ctx echo.Context, store sessions.Store) {
+ ctx.Set(context.SessionKey, store)
+}
diff --git a/pkg/session/session_test.go b/pkg/session/session_test.go
new file mode 100644
index 0000000..62d5d89
--- /dev/null
+++ b/pkg/session/session_test.go
@@ -0,0 +1,23 @@
+package session
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gorilla/sessions"
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetStore(t *testing.T) {
+ e := echo.New()
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ ctx := e.NewContext(req, httptest.NewRecorder())
+ _, err := Get(ctx, "test")
+ assert.Equal(t, ErrStoreNotFound, err)
+
+ Store(ctx, sessions.NewCookieStore([]byte("secret")))
+ _, err = Get(ctx, "test")
+ assert.NoError(t, err)
+}
diff --git a/pkg/tasks/example.go b/pkg/tasks/example.go
new file mode 100644
index 0000000..4c7a86d
--- /dev/null
+++ b/pkg/tasks/example.go
@@ -0,0 +1,53 @@
+package tasks
+
+import (
+ "context"
+ "time"
+
+ "github.com/mikestefanello/backlite"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "github.com/mikestefanello/pagoda/pkg/services"
+)
+
+// ExampleTask is an example implementation of backlite.Task.
+// This represents the task that can be queued for execution via the task client and should contain everything
+// that your queue processor needs to process the task.
+type ExampleTask struct {
+ Message string
+}
+
+// Config satisfies the backlite.Task interface by providing configuration for the queue that these items will be
+// placed into for execution.
+func (t ExampleTask) Config() backlite.QueueConfig {
+ return backlite.QueueConfig{
+ Name: "ExampleTask",
+ MaxAttempts: 3,
+ Timeout: 5 * time.Second,
+ Backoff: 10 * time.Second,
+ Retention: &backlite.Retention{
+ Duration: 24 * time.Hour,
+ OnlyFailed: false,
+ Data: &backlite.RetainData{
+ OnlyFailed: false,
+ },
+ },
+ }
+}
+
+// NewExampleTaskQueue provides a Queue that can process ExampleTask tasks.
+// The service container is provided so the subscriber can have access to the app dependencies.
+// All queues must be registered in the Register() function.
+// Whenever an ExampleTask is added to the task client, it will be queued and eventually sent here for execution.
+func NewExampleTaskQueue(c *services.Container) backlite.Queue {
+ return backlite.NewQueue[ExampleTask](func(ctx context.Context, task ExampleTask) error {
+ log.Default().Info("Example task received",
+ "message", task.Message,
+ )
+ log.Default().Info("This can access the container for dependencies",
+ "echo", c.Web.Reverse(routenames.Home),
+ )
+ return nil
+ })
+}
diff --git a/pkg/tasks/register.go b/pkg/tasks/register.go
new file mode 100644
index 0000000..fc934dc
--- /dev/null
+++ b/pkg/tasks/register.go
@@ -0,0 +1,10 @@
+package tasks
+
+import (
+ "github.com/mikestefanello/pagoda/pkg/services"
+)
+
+// Register registers all task queues with the task client.
+func Register(c *services.Container) {
+ c.Tasks.Register(NewExampleTaskQueue(c))
+}
diff --git a/tests/tests.go b/pkg/tests/tests.go
similarity index 54%
rename from tests/tests.go
rename to pkg/tests/tests.go
index 5c4a684..17dec0d 100644
--- a/tests/tests.go
+++ b/pkg/tests/tests.go
@@ -10,27 +10,29 @@ import (
"testing"
"time"
- "goweb/ent"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/pkg/session"
- "github.com/go-playground/assert/v2"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gorilla/sessions"
- "github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
+// NewContext creates a new Echo context for tests using an HTTP test request and response recorder
func NewContext(e *echo.Echo, url string) (echo.Context, *httptest.ResponseRecorder) {
req := httptest.NewRequest(http.MethodGet, url, strings.NewReader(""))
rec := httptest.NewRecorder()
return e.NewContext(req, rec), rec
}
+// InitSession initializes a session for a given Echo context
func InitSession(ctx echo.Context) {
- mw := session.Middleware(sessions.NewCookieStore([]byte("secret")))
- _ = ExecuteMiddleware(ctx, mw)
+ session.Store(ctx, sessions.NewCookieStore([]byte("secret")))
}
+// ExecuteMiddleware executes a middleware function on a given Echo context
func ExecuteMiddleware(ctx echo.Context, mw echo.MiddlewareFunc) error {
handler := mw(func(c echo.Context) error {
return nil
@@ -38,12 +40,29 @@ func ExecuteMiddleware(ctx echo.Context, mw echo.MiddlewareFunc) error {
return handler(ctx)
}
+// ExecuteHandler executes a handler with an optional stack of middleware
+func ExecuteHandler(ctx echo.Context, handler echo.HandlerFunc, mw ...echo.MiddlewareFunc) error {
+ return ExecuteMiddleware(ctx, func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(ctx echo.Context) error {
+ run := handler
+
+ for _, w := range mw {
+ run = w(run)
+ }
+
+ return run(ctx)
+ }
+ })
+}
+
+// AssertHTTPErrorCode asserts an HTTP status code on a given Echo HTTP error
func AssertHTTPErrorCode(t *testing.T, err error, code int) {
httpError, ok := err.(*echo.HTTPError)
require.True(t, ok)
assert.Equal(t, code, httpError.Code)
}
+// CreateUser creates a random user entity
func CreateUser(orm *ent.Client) (*ent.User, error) {
seed := fmt.Sprintf("%d-%d", time.Now().UnixMilli(), rand.Intn(1000000))
return orm.User.
diff --git a/pkg/ui/cache/cache.go b/pkg/ui/cache/cache.go
new file mode 100644
index 0000000..ec4eaee
--- /dev/null
+++ b/pkg/ui/cache/cache.go
@@ -0,0 +1,69 @@
+package cache
+
+import (
+ "bytes"
+ "sync"
+
+ "github.com/mikestefanello/pagoda/pkg/log"
+ "maragu.dev/gomponents"
+)
+
+var (
+ // cache stores a cache of assembled components by key.
+ cache = make(map[string]gomponents.Node)
+
+ // mu handles concurrent access to the cache.
+ mu sync.RWMutex
+)
+
+// Set sets a given renderable node in the cache with a given key.
+// You should only cache nodes that are entirely static.
+// This will panic if the node fails to render.
+//
+// To optimize performance, the node will be rendered and converted to a Raw component so the assembly and rendering
+// of the entire, nested node only has to execute once. This can eliminate countless function calls and string building.
+// It's worth noting that this performance optimization is, in most cases, entirely unnecessary, but it's easy to do
+// and realize some performance gains. In my very limited testing, gomponents actually outperformed Go templates in
+// many areas; but not all. The results were still very close and my limited testing is in no way definitive.
+//
+// In most applications, these slight differences in nanoseconds and bytes allocated will almost never matter or even
+// be noticeable, but it's good to be aware of them; and it's fun to address them. In looking at the example layouts
+// provided, I noticed that a lot of nested function calls and string building was happening on every single page load
+// just to re-render static HTML such as the navbar and the search form/modal. Benchmarks quickly revealed that caching
+// those high-level nodes made a significant difference in speed and memory allocations. Going further, I thought that
+// with the entire node cached, you still have to render the entire nested structure each time it's used, so that is why
+// this will render them upfront, then cache. If my few examples have a handful of static nodes, I assume most full
+// applications will have many, so maybe this is useful.
+func Set(key string, node gomponents.Node) {
+ buf := bytes.NewBuffer(nil)
+ if err := node.Render(buf); err != nil {
+ log.Default().Error("failed to cache ui node",
+ "error", err,
+ "key", key,
+ )
+ return
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+ cache[key] = gomponents.Raw(buf.String())
+}
+
+// Get returns the node cached under the provided key, if one exists.
+func Get(key string) gomponents.Node {
+ mu.RLock()
+ defer mu.RUnlock()
+ return cache[key]
+}
+
+// SetIfNotExists will return the cached Node for the key, if it exists, otherwise it will use the provided callback
+// function to generate the node and cache it.
+func SetIfNotExists(key string, gen func() gomponents.Node) gomponents.Node {
+ if n := Get(key); n != nil {
+ return n
+ }
+
+ n := gen()
+ Set(key, n)
+ return n
+}
diff --git a/pkg/ui/cache/cache_test.go b/pkg/ui/cache/cache_test.go
new file mode 100644
index 0000000..06b0c94
--- /dev/null
+++ b/pkg/ui/cache/cache_test.go
@@ -0,0 +1,57 @@
+package cache
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func TestCache_GetSet(t *testing.T) {
+ key := "test"
+ assert.Nil(t, Get(key))
+
+ node := Div(Text("hello"))
+ Set(key, node)
+
+ got := Get(key)
+ require.NotNil(t, got)
+
+ // Check it was converted to a Raw component.
+ _, ok := got.(NodeFunc)
+ require.True(t, ok)
+
+ // Both nodes should render the same string.
+ buf1, buf2 := bytes.NewBuffer(nil), bytes.NewBuffer(nil)
+ require.NoError(t, node.Render(buf1))
+ require.NoError(t, got.Render(buf2))
+ assert.Equal(t, buf1.String(), buf2.String())
+}
+
+func TestCache_SetIfNotExists(t *testing.T) {
+ key := "test2"
+ called := 0
+ callback := func() Node {
+ called++
+ return Div(Text("hello"))
+ }
+
+ assertRender := func(n Node) {
+ buf := bytes.NewBuffer(nil)
+ require.NoError(t, n.Render(buf))
+ assert.Equal(t, `
hello
`, buf.String())
+ }
+
+ got := SetIfNotExists(key, callback)
+ assert.Equal(t, 1, called)
+ require.NotNil(t, got)
+ assertRender(got)
+
+ got = SetIfNotExists(key, callback)
+ assert.Equal(t, 1, called)
+ require.NotNil(t, got)
+ assertRender(got)
+}
diff --git a/pkg/ui/components/alerts.go b/pkg/ui/components/alerts.go
new file mode 100644
index 0000000..ae381be
--- /dev/null
+++ b/pkg/ui/components/alerts.go
@@ -0,0 +1,66 @@
+package components
+
+import (
+ "github.com/mikestefanello/pagoda/pkg/msg"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ "github.com/mikestefanello/pagoda/pkg/ui/icons"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func FlashMessages(r *ui.Request) Node {
+ var g Group
+ var color Color
+
+ for _, typ := range []msg.Type{
+ msg.TypeSuccess,
+ msg.TypeInfo,
+ msg.TypeWarning,
+ msg.TypeError,
+ } {
+ for _, str := range msg.Get(r.Context, typ) {
+ switch typ {
+ case msg.TypeSuccess:
+ color = ColorSuccess
+ case msg.TypeInfo:
+ color = ColorInfo
+ case msg.TypeWarning:
+ color = ColorWarning
+ case msg.TypeError:
+ color = ColorError
+ }
+
+ g = append(g, Alert(color, str))
+ }
+ }
+
+ return g
+}
+
+func Alert(color Color, text string) Node {
+ var class string
+
+ switch color {
+ case ColorSuccess:
+ class = "alert-success"
+ case ColorInfo:
+ class = "alert-info"
+ case ColorWarning:
+ class = "alert-warning"
+ case ColorError:
+ class = "alert-error"
+ }
+
+ return Div(
+ Role("alert"),
+ Class("alert mb-2 "+class),
+ Attr("x-data", "{show: true}"),
+ Attr("x-show", "show"),
+ Span(
+ Attr("@click", "show = false"),
+ Class("cursor-pointer"),
+ icons.XCircle(),
+ ),
+ Span(Text(text)),
+ )
+}
diff --git a/pkg/ui/components/data.go b/pkg/ui/components/data.go
new file mode 100644
index 0000000..95b4d25
--- /dev/null
+++ b/pkg/ui/components/data.go
@@ -0,0 +1,121 @@
+package components
+
+import (
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type (
+ CardParams struct {
+ Title string
+ Body Group
+ Footer Group
+ Color Color
+ Size Size
+ }
+
+ Stat struct {
+ Title string
+ Value string
+ Description string
+ Icon Node
+ }
+)
+
+func Badge(color Color, text string) Node {
+ var class string
+
+ switch color {
+ case ColorSuccess:
+ class = "badge-success"
+ case ColorWarning:
+ class = "badge-warning"
+ }
+
+ return Div(
+ Class("badge "+class),
+ Text(text),
+ )
+}
+
+func Divider(text string) Node {
+ return Div(
+ Class("divider"),
+ Text(text),
+ )
+}
+
+func Card(params CardParams) Node {
+ var colorClass, sizeClass string
+
+ switch params.Color {
+ case ColorSuccess:
+ colorClass = "bg-success text-success-content"
+ case ColorPrimary:
+ colorClass = "bg-primary text-primary-content"
+ case ColorAccent:
+ colorClass = "bg-accent text-accent-content"
+ case ColorNeutral:
+ colorClass = "bg-neutral text-neutral-content"
+ case ColorWarning:
+ colorClass = "bg-warning text-warning-content"
+ case ColorInfo:
+ colorClass = "bg-info text-info-content"
+ }
+
+ switch params.Size {
+ case SizeSmall:
+ sizeClass = "card-sm"
+ case SizeMedium:
+ sizeClass = "card-md"
+ case SizeLarge:
+ sizeClass = "card-lg"
+ }
+
+ return Div(
+ Class("cards mb-2 "+colorClass+" "+sizeClass),
+ Div(
+ Class("card-body"),
+ If(len(params.Title) > 0, Span(
+ Class("card-title"),
+ Text(params.Title),
+ )),
+ params.Body,
+ If(params.Footer != nil, Div(
+ Class("card-actions justify-end"),
+ params.Footer,
+ )),
+ ),
+ )
+}
+
+func Stats(stats ...Stat) Node {
+ g := make(Group, 0, len(stats))
+ for _, stat := range stats {
+ g = append(g, Div(
+ Class("stat"),
+ Iff(stat.Icon != nil, func() Node {
+ return Div(
+ Class("stat-figure text-secondary"),
+ stat.Icon,
+ )
+ }),
+ Div(
+ Class("stat-title"),
+ Text(stat.Title),
+ ),
+ Div(
+ Class("stat-value"),
+ Text(stat.Value),
+ ),
+ Div(
+ Class("stat-desc"),
+ Text(stat.Description),
+ ),
+ ))
+ }
+ return Div(
+ Class("stats shadow"),
+ g,
+ )
+}
diff --git a/pkg/ui/components/form.go b/pkg/ui/components/form.go
new file mode 100644
index 0000000..1992d3e
--- /dev/null
+++ b/pkg/ui/components/form.go
@@ -0,0 +1,268 @@
+package components
+
+import (
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type (
+ InputFieldParams struct {
+ Form form.Form
+ FormField string
+ Name string
+ InputType string
+ Label string
+ Value string
+ Placeholder string
+ Help string
+ }
+
+ FileFieldParams struct {
+ Name string
+ Label string
+ Help string
+ }
+
+ OptionsParams struct {
+ Form form.Form
+ FormField string
+ Name string
+ Label string
+ Value string
+ Options []Choice
+ Help string
+ }
+
+ Choice struct {
+ Value string
+ Label string
+ }
+
+ TextareaFieldParams struct {
+ Form form.Form
+ FormField string
+ Name string
+ Label string
+ Value string
+ Help string
+ }
+
+ CheckboxParams struct {
+ Form form.Form
+ FormField string
+ Name string
+ Label string
+ Checked bool
+ }
+)
+
+func ControlGroup(controls ...Node) Node {
+ return Div(
+ Class("mt-2 flex gap-2"),
+ Group(controls),
+ )
+}
+
+func TextareaField(el TextareaFieldParams) Node {
+ return Fieldset(
+ el.Label,
+ Textarea(
+ Class("textarea h-24 w-2/3 "+formFieldStatusClass(el.Form, el.FormField)),
+ ID(el.Name),
+ Name(el.Name),
+ Text(el.Value),
+ ),
+ Help(el.Help),
+ formFieldErrors(el.Form, el.FormField),
+ )
+}
+
+func Radios(el OptionsParams) Node {
+ buttons := make(Group, len(el.Options))
+ for i, opt := range el.Options {
+ id := "radio-" + el.Name + "-" + opt.Value
+ buttons[i] = Div(
+ Class("mb-2"),
+ Input(
+ ID(id),
+ Type("radio"),
+ Name(el.Name),
+ Value(opt.Value),
+ Class("radio mr-1 "+formFieldStatusClass(el.Form, el.FormField)),
+ If(el.Value == opt.Value, Checked()),
+ ),
+ Label(
+ Text(opt.Label),
+ For(id),
+ ),
+ )
+ }
+
+ return Fieldset(
+ el.Label,
+ buttons,
+ formFieldErrors(el.Form, el.FormField),
+ )
+}
+
+func SelectList(el OptionsParams) Node {
+ buttons := make(Group, len(el.Options))
+ for i, opt := range el.Options {
+ buttons[i] = Option(
+ Text(opt.Label),
+ Value(opt.Value),
+ If(opt.Value == el.Value, Attr("selected")),
+ )
+ }
+
+ return Fieldset(
+ el.Label,
+ Select(
+ Class("select "+formFieldStatusClass(el.Form, el.FormField)),
+ Name(el.Name),
+ buttons,
+ ),
+ Help(el.Help),
+ formFieldErrors(el.Form, el.FormField),
+ )
+}
+
+func Checkbox(el CheckboxParams) Node {
+ return Div(
+ Label(
+ Class("label"),
+ Input(
+ Class("checkbox"),
+ Type("checkbox"),
+ Name(el.Name),
+ If(el.Checked, Checked()),
+ Value("true"),
+ ),
+ Text(" "+el.Label),
+ ),
+ formFieldErrors(el.Form, el.FormField),
+ )
+}
+
+func InputField(el InputFieldParams) Node {
+ return Fieldset(
+ el.Label,
+ Input(
+ ID(el.Name),
+ Name(el.Name),
+ Type(el.InputType),
+ Class("input "+formFieldStatusClass(el.Form, el.FormField)),
+ Value(el.Value),
+ If(el.Placeholder != "", Placeholder(el.Placeholder)),
+ ),
+ Help(el.Help),
+ formFieldErrors(el.Form, el.FormField),
+ )
+}
+
+func Help(text string) Node {
+ return If(len(text) > 0, Div(
+ Class("label"),
+ Text(text),
+ ))
+}
+
+func Fieldset(label string, els ...Node) Node {
+ return FieldSet(
+ Class("fieldset"),
+ If(len(label) > 0, Legend(
+ Class("fieldset-legend"),
+ Text(label),
+ )),
+ Group(els),
+ )
+}
+
+func FileField(el FileFieldParams) Node {
+ return Fieldset(
+ el.Label,
+ Input(
+ Type("file"),
+ Class("file-input"),
+ Name(el.Name),
+ ),
+ Help(el.Help),
+ )
+}
+
+func formFieldStatusClass(fm form.Form, formField string) string {
+ switch {
+ case fm == nil:
+ return ""
+ case !fm.IsSubmitted():
+ return ""
+ case fm.FieldHasErrors(formField):
+ return "input-error"
+ default:
+ return "input-success"
+ }
+}
+
+func formFieldErrors(fm form.Form, field string) Node {
+ if fm == nil {
+ return nil
+ }
+
+ errs := fm.GetFieldErrors(field)
+ if len(errs) == 0 {
+ return nil
+ }
+
+ g := make(Group, len(errs))
+ for i, err := range errs {
+ g[i] = Div(
+ Class("text-error"),
+ Text(err),
+ )
+ }
+
+ return g
+}
+
+func CSRF(r *ui.Request) Node {
+ return Input(
+ Type("hidden"),
+ Name("csrf"),
+ Value(r.CSRF),
+ )
+}
+
+func FormButton(color Color, label string) Node {
+ return Button(
+ Class("btn "+buttonColor(color)),
+ Text(label),
+ )
+}
+
+func ButtonLink(color Color, href, label string) Node {
+ return A(
+ Href(href),
+ Class("btn "+buttonColor(color)),
+ Text(label),
+ )
+}
+
+func buttonColor(color Color) string {
+ // Only colors being used are included so unused styles are not compiled.
+ switch color {
+ case ColorPrimary:
+ return "btn-primary"
+ case ColorInfo:
+ return "btn-info"
+ case ColorAccent:
+ return "btn-accent"
+ case ColorError:
+ return "btn-error"
+ case ColorLink:
+ return "btn-link"
+ default:
+ return ""
+ }
+}
diff --git a/pkg/ui/components/head.go b/pkg/ui/components/head.go
new file mode 100644
index 0000000..879a986
--- /dev/null
+++ b/pkg/ui/components/head.go
@@ -0,0 +1,35 @@
+package components
+
+import (
+ "strings"
+
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func JS() Node {
+ return Group{
+ Script(Src("https://unpkg.com/htmx.org@2.0.0/dist/htmx.min.js"), Defer()),
+ Script(Src("https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"), Defer()),
+ }
+}
+
+func CSS() Node {
+ return Link(
+ Href(ui.StaticFile("main.css")),
+ Rel("stylesheet"),
+ Type("text/css"),
+ )
+}
+
+func Metatags(r *ui.Request) Node {
+ return Group{
+ Meta(Charset("utf-8")),
+ Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
+ Link(Rel("icon"), Href(ui.StaticFile("favicon.png"))),
+ TitleEl(Text(r.Config.App.Name), If(r.Title != "", Text(" | "+r.Title))),
+ If(r.Metatags.Description != "", Meta(Name("description"), Content(r.Metatags.Description))),
+ If(len(r.Metatags.Keywords) > 0, Meta(Name("keywords"), Content(strings.Join(r.Metatags.Keywords, ", ")))),
+ }
+}
diff --git a/pkg/ui/components/htmx.go b/pkg/ui/components/htmx.go
new file mode 100644
index 0000000..e4f97f0
--- /dev/null
+++ b/pkg/ui/components/htmx.go
@@ -0,0 +1,39 @@
+package components
+
+import (
+ "fmt"
+
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func HtmxListeners(r *ui.Request) Node {
+ const htmxErr = `
+ document.body.addEventListener('htmx:beforeSwap', function(evt) {
+ if (evt.detail.xhr.status >= 400){
+ evt.detail.shouldSwap = true;
+ evt.detail.target = htmx.find("body");
+ }
+ });
+ `
+
+ const htmxCSRF = `
+ document.body.addEventListener('htmx:configRequest', function(evt) {
+ if (evt.detail.verb !== "get") {
+ evt.detail.parameters['csrf'] = '%s';
+ }
+ })
+ `
+
+ return Group{
+ Script(Raw(htmxErr)),
+ Iff(len(r.CSRF) > 0, func() Node {
+ return Script(Raw(fmt.Sprintf(htmxCSRF, r.CSRF)))
+ }),
+ }
+}
+
+func HxBoost() Node {
+ return Attr("hx-boost", "true")
+}
diff --git a/pkg/ui/components/nav.go b/pkg/ui/components/nav.go
new file mode 100644
index 0000000..04fd627
--- /dev/null
+++ b/pkg/ui/components/nav.go
@@ -0,0 +1,72 @@
+package components
+
+import (
+ "fmt"
+
+ "github.com/mikestefanello/pagoda/pkg/pager"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/components"
+ . "maragu.dev/gomponents/html"
+)
+
+func MenuLink(r *ui.Request, icon Node, title, routeName string, routeParams ...any) Node {
+ href := r.Path(routeName, routeParams...)
+
+ return Li(
+ Class("ml-2"),
+ A(
+ Href(href),
+ icon,
+ Text(title),
+ Classes{
+ "menu-active": href == r.CurrentPath,
+ "p-2": true,
+ },
+ ),
+ )
+}
+
+func Pager(page int, path string, hasNext bool, hxTarget string) Node {
+ href := func(page int) string {
+ return fmt.Sprintf("%s?%s=%d",
+ path,
+ pager.QueryKey,
+ page,
+ )
+ }
+
+ return Div(
+ Class("join"),
+ A(
+ Class("join-item btn"),
+ Text("«"),
+ If(page <= 1, Disabled()),
+ Href(href(page-1)),
+ Iff(len(hxTarget) > 0, func() Node {
+ return Group{
+ Attr("hx-get", href(page-1)),
+ Attr("hx-swap", "outerHTML"),
+ Attr("hx-target", hxTarget),
+ }
+ }),
+ ),
+ Button(
+ Class("join-item btn"),
+ Textf("Page %d", page),
+ ),
+ A(
+ Class("join-item btn"),
+ Text("»"),
+ If(!hasNext, Disabled()),
+ Href(href(page+1)),
+ Iff(len(hxTarget) > 0, func() Node {
+ return Group{
+ Attr("hx-get", href(page+1)),
+ Attr("hx-swap", "outerHTML"),
+ Attr("hx-target", hxTarget),
+ }
+ }),
+ ),
+ )
+}
diff --git a/pkg/ui/components/styles.go b/pkg/ui/components/styles.go
new file mode 100644
index 0000000..5a8696e
--- /dev/null
+++ b/pkg/ui/components/styles.go
@@ -0,0 +1,27 @@
+package components
+
+type (
+ Color int
+ Size int
+)
+
+const (
+ ColorNone Color = iota
+ ColorNeutral
+ ColorPrimary
+ ColorSecondary
+ ColorAccent
+ ColorInfo
+ ColorSuccess
+ ColorWarning
+ ColorError
+ ColorLink
+)
+
+const (
+ SizeExtraSmall Size = iota
+ SizeSmall
+ SizeMedium
+ SizeLarge
+ SizeExtraLarge
+)
diff --git a/pkg/ui/components/tabs.go b/pkg/ui/components/tabs.go
new file mode 100644
index 0000000..d1efdc0
--- /dev/null
+++ b/pkg/ui/components/tabs.go
@@ -0,0 +1,38 @@
+package components
+
+import (
+ "fmt"
+ "math/rand"
+
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type Tab struct {
+ Title, Body string
+}
+
+func Tabs(tabs []Tab) Node {
+ g := make(Group, 0, len(tabs)*2)
+ id := fmt.Sprintf("tabs-%d", rand.Int())
+
+ for i, tab := range tabs {
+ g = append(g,
+ Input(
+ Type("radio"),
+ Name(id),
+ Class("tab"),
+ Aria("label", tab.Title),
+ If(i == 0, Checked()),
+ ),
+ Div(
+ Class("tab-content bg-base-100 border-base-300 p-6"),
+ Raw(tab.Body),
+ ))
+ }
+
+ return Div(
+ Class("tabs tabs-lift"),
+ g,
+ )
+}
diff --git a/pkg/ui/emails/auth.go b/pkg/ui/emails/auth.go
new file mode 100644
index 0000000..771f854
--- /dev/null
+++ b/pkg/ui/emails/auth.go
@@ -0,0 +1,22 @@
+package emails
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func ConfirmEmailAddress(ctx echo.Context, username, token string) Node {
+ url := ui.NewRequest(ctx).
+ Url(routenames.VerifyEmail, token)
+
+ return Group{
+ Strong(Textf("Hello %s,", username)),
+ Br(),
+ P(Text("Please click on the following link to confirm your email address:")),
+ Br(),
+ A(Href(url), Text(url)),
+ }
+}
diff --git a/pkg/ui/forms/admin_entity.go b/pkg/ui/forms/admin_entity.go
new file mode 100644
index 0000000..cf3ea37
--- /dev/null
+++ b/pkg/ui/forms/admin_entity.go
@@ -0,0 +1,124 @@
+package forms
+
+import (
+ "net/http"
+ "net/url"
+
+ "entgo.io/ent/schema/field"
+ "github.com/mikestefanello/pagoda/ent/admin"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func AdminEntity(r *ui.Request, entityType admin.EntityType, values url.Values) Node {
+ // TODO inline validation?
+ isNew := values == nil
+ nodes := make(Group, 0)
+
+ getValue := func(name string) string {
+ // Values in the submitted form take precedence.
+ if value := r.Context.FormValue(name); value != "" {
+ return value
+ }
+
+ // Fallback to the entity's values, if being edited.
+ if values != nil && len(values[name]) > 0 {
+ return values[name][0]
+ }
+
+ return ""
+ }
+
+ // Attempt to add form elements for all editable entity fields.
+ for _, f := range entityType.GetSchema() {
+ // TODO cardinality?
+ if !isNew && f.Immutable {
+ continue
+ }
+
+ switch f.Type {
+ case field.TypeString:
+ p := InputFieldParams{
+ Name: f.Name,
+ InputType: "text",
+ Label: admin.FieldLabel(f.Name),
+ Value: getValue(f.Name),
+ }
+
+ if f.Sensitive {
+ p.InputType = "password"
+ if !isNew {
+ p.Placeholder = "*****"
+ p.Help = "SENSITIVE: This field will only be updated if a value is provided."
+ }
+ }
+ nodes = append(nodes, InputField(p))
+
+ case field.TypeTime:
+ nodes = append(nodes, InputField(InputFieldParams{
+ Name: f.Name,
+ InputType: "datetime-local",
+ Label: admin.FieldLabel(f.Name),
+ Value: getValue(f.Name),
+ }))
+
+ case field.TypeInt, field.TypeInt8, field.TypeInt16, field.TypeInt32, field.TypeInt64,
+ field.TypeUint, field.TypeUint8, field.TypeUint16, field.TypeUint32, field.TypeUint64,
+ field.TypeFloat32, field.TypeFloat64:
+ nodes = append(nodes, InputField(InputFieldParams{
+ Name: f.Name,
+ InputType: "number",
+ Label: admin.FieldLabel(f.Name),
+ Value: getValue(f.Name),
+ }))
+
+ case field.TypeBool:
+ nodes = append(nodes, Checkbox(CheckboxParams{
+ Name: f.Name,
+ Label: admin.FieldLabel(f.Name),
+ Checked: getValue(f.Name) == "true",
+ }))
+
+ case field.TypeEnum:
+ options := make([]Choice, 0, len(f.Enums)+1)
+ if f.Optional {
+ options = append(options, Choice{
+ Label: "-",
+ Value: "",
+ })
+ }
+ for _, enum := range f.Enums {
+ options = append(options, Choice{
+ Label: enum,
+ Value: enum,
+ })
+ }
+ nodes = append(nodes, SelectList(OptionsParams{
+ Name: f.Name,
+ Label: admin.FieldLabel(f.Name),
+ Value: getValue(f.Name),
+ Options: options,
+ }))
+
+ default:
+ nodes = append(nodes, P(Textf("%s not supported", f.Name)))
+ }
+ }
+
+ return Form(
+ Method(http.MethodPost),
+ nodes,
+ ControlGroup(
+ FormButton(ColorPrimary, "Submit"),
+ ButtonLink(
+ ColorNone,
+ r.Path(routenames.AdminEntityList(entityType.GetName())),
+ "Cancel",
+ ),
+ ),
+ CSRF(r),
+ )
+}
diff --git a/pkg/ui/forms/admin_entity_delete.go b/pkg/ui/forms/admin_entity_delete.go
new file mode 100644
index 0000000..36ad49f
--- /dev/null
+++ b/pkg/ui/forms/admin_entity_delete.go
@@ -0,0 +1,30 @@
+package forms
+
+import (
+ "net/http"
+
+ "github.com/mikestefanello/pagoda/ent/admin"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func AdminEntityDelete(r *ui.Request, entityType admin.EntityType) Node {
+ return Form(
+ Method(http.MethodPost),
+ P(
+ Textf("Are you sure you want to delete this %s?", entityType.GetName()),
+ ),
+ ControlGroup(
+ FormButton(ColorError, "Delete"),
+ ButtonLink(
+ ColorNone,
+ r.Path(routenames.AdminEntityList(entityType.GetName())),
+ "Cancel",
+ ),
+ ),
+ CSRF(r),
+ )
+}
diff --git a/pkg/ui/forms/cache.go b/pkg/ui/forms/cache.go
new file mode 100644
index 0000000..c2d77dc
--- /dev/null
+++ b/pkg/ui/forms/cache.go
@@ -0,0 +1,54 @@
+package forms
+
+import (
+ "net/http"
+
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type Cache struct {
+ CurrentValue string
+ Value string `form:"value"`
+ form.Submission
+}
+
+func (f *Cache) Render(r *ui.Request) Node {
+ return Form(
+ ID("cache"),
+ Method(http.MethodPost),
+ Attr("hx-post", r.Path(routenames.CacheSubmit)),
+ Card(CardParams{
+ Title: "Test the cache",
+ Body: Group{
+ Span(Text("This route handler shows how the default in-memory cache works. Try updating the value using the form below and see how it persists after you reload the page.")),
+ Span(Text("HTMX makes it easy to re-render the cached value after the form is submitted.")),
+ },
+ Color: ColorInfo,
+ Size: SizeMedium,
+ }),
+ Label(
+ For("value"),
+ Class("value"),
+ Text("Value in cache: "),
+ ),
+ If(f.CurrentValue != "", Badge(ColorSuccess, f.CurrentValue)),
+ If(f.CurrentValue == "", Badge(ColorWarning, "empty")),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Value",
+ Name: "value",
+ InputType: "text",
+ Label: "Value",
+ Value: f.Value,
+ }),
+ ControlGroup(
+ FormButton(ColorPrimary, "Update cache"),
+ ),
+ CSRF(r),
+ )
+}
diff --git a/pkg/ui/forms/contact.go b/pkg/ui/forms/contact.go
new file mode 100644
index 0000000..44def5e
--- /dev/null
+++ b/pkg/ui/forms/contact.go
@@ -0,0 +1,58 @@
+package forms
+
+import (
+ "net/http"
+
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type Contact struct {
+ Email string `form:"email" validate:"required,email"`
+ Department string `form:"department" validate:"required,oneof=sales marketing hr"`
+ Message string `form:"message" validate:"required"`
+ form.Submission
+}
+
+func (f *Contact) Render(r *ui.Request) Node {
+ return Form(
+ ID("contact"),
+ Method(http.MethodPost),
+ Attr("hx-post", r.Path(routenames.ContactSubmit)),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Email",
+ Name: "email",
+ InputType: "email",
+ Label: "Email address",
+ Value: f.Email,
+ }),
+ Radios(OptionsParams{
+ Form: f,
+ FormField: "Department",
+ Name: "department",
+ Label: "Department",
+ Value: f.Department,
+ Options: []Choice{
+ {Value: "sales", Label: "Sales"},
+ {Value: "marketing", Label: "Marketing"},
+ {Value: "hr", Label: "HR"},
+ },
+ }),
+ TextareaField(TextareaFieldParams{
+ Form: f,
+ FormField: "Message",
+ Name: "message",
+ Label: "Message",
+ Value: f.Message,
+ }),
+ ControlGroup(
+ FormButton(ColorPrimary, "Submit"),
+ ),
+ CSRF(r),
+ )
+}
diff --git a/pkg/ui/forms/file.go b/pkg/ui/forms/file.go
new file mode 100644
index 0000000..1b15cc2
--- /dev/null
+++ b/pkg/ui/forms/file.go
@@ -0,0 +1,31 @@
+package forms
+
+import (
+ "net/http"
+
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type File struct{}
+
+func (f File) Render(r *ui.Request) Node {
+ return Form(
+ ID("files"),
+ Method(http.MethodPost),
+ Action(r.Path(routenames.FilesSubmit)),
+ EncType("multipart/form-data"),
+ FileField(FileFieldParams{
+ Name: "file",
+ Label: "Test file",
+ Help: "Pick a file to upload.",
+ }),
+ ControlGroup(
+ FormButton(ColorPrimary, "Upload"),
+ ),
+ CSRF(r),
+ )
+}
diff --git a/pkg/ui/forms/forgot_password.go b/pkg/ui/forms/forgot_password.go
new file mode 100644
index 0000000..91f06cc
--- /dev/null
+++ b/pkg/ui/forms/forgot_password.go
@@ -0,0 +1,39 @@
+package forms
+
+import (
+ "net/http"
+
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type ForgotPassword struct {
+ Email string `form:"email" validate:"required,email"`
+ form.Submission
+}
+
+func (f *ForgotPassword) Render(r *ui.Request) Node {
+ return Form(
+ ID("forgot-password"),
+ Method(http.MethodPost),
+ HxBoost(),
+ Action(r.Path(routenames.ForgotPasswordSubmit)),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Email",
+ Name: "email",
+ InputType: "email",
+ Label: "Email address",
+ Value: f.Email,
+ }),
+ ControlGroup(
+ FormButton(ColorPrimary, "Reset password"),
+ ButtonLink(ColorLink, r.Path(routenames.Home), "Cancel"),
+ ),
+ CSRF(r),
+ )
+}
diff --git a/pkg/ui/forms/login.go b/pkg/ui/forms/login.go
new file mode 100644
index 0000000..c5921c0
--- /dev/null
+++ b/pkg/ui/forms/login.go
@@ -0,0 +1,64 @@
+package forms
+
+import (
+ "net/http"
+
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type Login struct {
+ Email string `form:"email" validate:"required,email"`
+ Password string `form:"password" validate:"required"`
+ form.Submission
+}
+
+func (f *Login) Render(r *ui.Request) Node {
+ return Form(
+ ID("login"),
+ Method(http.MethodPost),
+ HxBoost(),
+ Action(r.Path(routenames.LoginSubmit)),
+ FlashMessages(r),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Email",
+ Name: "email",
+ InputType: "email",
+ Label: "Email address",
+ Value: f.Email,
+ }),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Password",
+ Name: "password",
+ InputType: "password",
+ Label: "Password",
+ Placeholder: "******",
+ }),
+ Div(
+ Class("text-right text-primary mt-2"),
+ A(
+ Href(r.Path(routenames.ForgotPassword)),
+ Text("Forgot password?"),
+ ),
+ ),
+ ControlGroup(
+ FormButton(ColorPrimary, "Login"),
+ ButtonLink(ColorLink, r.Path(routenames.Home), "Cancel"),
+ ),
+ CSRF(r),
+ Div(
+ Class("text-center text-base-content/50 mt-4"),
+ Text("Don't have an account? "),
+ A(
+ Href(r.Path(routenames.Register)),
+ Text("Register"),
+ ),
+ ),
+ )
+}
diff --git a/pkg/ui/forms/register.go b/pkg/ui/forms/register.go
new file mode 100644
index 0000000..473c8f2
--- /dev/null
+++ b/pkg/ui/forms/register.go
@@ -0,0 +1,74 @@
+package forms
+
+import (
+ "net/http"
+
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type Register struct {
+ Name string `form:"name" validate:"required"`
+ Email string `form:"email" validate:"required,email"`
+ Password string `form:"password" validate:"required"`
+ ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"`
+ form.Submission
+}
+
+func (f *Register) Render(r *ui.Request) Node {
+ return Form(
+ ID("register"),
+ Method(http.MethodPost),
+ HxBoost(),
+ Action(r.Path(routenames.RegisterSubmit)),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Name",
+ Name: "name",
+ InputType: "text",
+ Label: "Name",
+ Value: f.Name,
+ }),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Email",
+ Name: "email",
+ InputType: "email",
+ Label: "Email address",
+ Value: f.Email,
+ }),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Password",
+ Name: "password",
+ InputType: "password",
+ Label: "Password",
+ Placeholder: "******",
+ }),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "ConfirmPassword",
+ Name: "password-confirm",
+ InputType: "password",
+ Label: "Confirm password",
+ Placeholder: "******",
+ }),
+ ControlGroup(
+ FormButton(ColorPrimary, "Register"),
+ ButtonLink(ColorLink, r.Path(routenames.Home), "Cancel"),
+ ),
+ CSRF(r),
+ Div(
+ Class("text-center text-base-content/50 mt-4"),
+ Text("Already have an account? "),
+ A(
+ Href(r.Path(routenames.Login)),
+ Text("Login"),
+ ),
+ ),
+ )
+}
diff --git a/pkg/ui/forms/reset_password.go b/pkg/ui/forms/reset_password.go
new file mode 100644
index 0000000..2fa2390
--- /dev/null
+++ b/pkg/ui/forms/reset_password.go
@@ -0,0 +1,46 @@
+package forms
+
+import (
+ "net/http"
+
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type ResetPassword struct {
+ Password string `form:"password" validate:"required"`
+ ConfirmPassword string `form:"password-confirm" validate:"required,eqfield=Password"`
+ form.Submission
+}
+
+func (f *ResetPassword) Render(r *ui.Request) Node {
+ return Form(
+ ID("reset-password"),
+ Method(http.MethodPost),
+ HxBoost(),
+ Action(r.CurrentPath),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Password",
+ Name: "password",
+ InputType: "password",
+ Label: "Password",
+ Placeholder: "******",
+ }),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "PasswordConfirm",
+ Name: "password-confirm",
+ InputType: "password",
+ Label: "Confirm password",
+ Placeholder: "******",
+ }),
+ ControlGroup(
+ FormButton(ColorPrimary, "Update password"),
+ ),
+ CSRF(r),
+ )
+}
diff --git a/pkg/ui/forms/task.go b/pkg/ui/forms/task.go
new file mode 100644
index 0000000..ade3694
--- /dev/null
+++ b/pkg/ui/forms/task.go
@@ -0,0 +1,49 @@
+package forms
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/mikestefanello/pagoda/pkg/form"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type Task struct {
+ Delay int `form:"delay" validate:"gte=0"`
+ Message string `form:"message" validate:"required"`
+ form.Submission
+}
+
+func (f *Task) Render(r *ui.Request) Node {
+ return Form(
+ ID("task"),
+ Method(http.MethodPost),
+ Attr("hx-post", r.Path(routenames.TaskSubmit)),
+ FlashMessages(r),
+ InputField(InputFieldParams{
+ Form: f,
+ FormField: "Delay",
+ Name: "delay",
+ InputType: "number",
+ Label: "Delay (in seconds)",
+ Help: "How long to wait until the task is executed",
+ Value: fmt.Sprint(f.Delay),
+ }),
+ TextareaField(TextareaFieldParams{
+ Form: f,
+ FormField: "Message",
+ Name: "message",
+ Label: "Message",
+ Value: f.Message,
+ Help: "The message the task will output to the log",
+ }),
+ ControlGroup(
+ FormButton(ColorPrimary, "Add task to queue"),
+ ),
+ CSRF(r),
+ )
+}
diff --git a/pkg/ui/icons/icons.go b/pkg/ui/icons/icons.go
new file mode 100644
index 0000000..c5ddf78
--- /dev/null
+++ b/pkg/ui/icons/icons.go
@@ -0,0 +1,208 @@
+package icons
+
+import (
+ "fmt"
+
+ "github.com/mikestefanello/pagoda/pkg/ui/cache"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func CircleStack() Node {
+ return icon("CircleStack",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"),
+ ),
+ )
+}
+
+func Eyes() Node {
+ return icon("Eyes",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"),
+ ),
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"),
+ ),
+ )
+}
+
+func UserCircle() Node {
+ return icon("UserCircle",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"),
+ ),
+ )
+}
+
+func Globe() Node {
+ return icon("Globe",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"),
+ ),
+ )
+}
+
+func Home() Node {
+ return icon("Home",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"),
+ ),
+ )
+}
+
+func Info() Node {
+ return icon("Info",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"),
+ ),
+ )
+}
+
+func Mail() Node {
+ return icon("Mail",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"),
+ ),
+ )
+}
+
+func Archive() Node {
+ return icon("Archive",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z"),
+ ),
+ )
+}
+
+func PencilSquare() Node {
+ return icon("PencilSquare",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"),
+ ),
+ )
+}
+
+func Document() Node {
+ return icon("Document",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"),
+ ),
+ )
+}
+
+func Exit() Node {
+ return icon("Exit",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15"),
+ ),
+ )
+}
+
+func Enter() Node {
+ return icon("Enter",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75"),
+ ),
+ )
+}
+
+func UserPlus() Node {
+ return icon("UserPlus",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"),
+ ),
+ )
+}
+
+func QuestionCircle() Node {
+ return icon("QuestionCircle",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"),
+ ),
+ )
+}
+
+func XCircle() Node {
+ return icon("XCircle",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"),
+ ),
+ )
+}
+
+func MagnifyingGlass() Node {
+ return icon("MagnifyingGlass",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"),
+ ),
+ )
+}
+
+func LockClosed() Node {
+ return icon("LockClosed",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"),
+ ),
+ )
+}
+
+func Star() Node {
+ return icon("Star",
+ El("path",
+ Attr("stroke-linecap", "round"),
+ Attr("stroke-linejoin", "round"),
+ Attr("d", "M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"),
+ ),
+ )
+}
+
+func icon(id string, els ...Node) Node {
+ return cache.SetIfNotExists(fmt.Sprintf("icon.%s", id), func() Node {
+ return SVG(
+ Attr("xmlns", "http://www.w3.org/2000/svg"),
+ Attr("fill", "none"),
+ Attr("viewBox", "0 0 24 24"),
+ Attr("stroke-width", "1.5"),
+ Attr("stroke", "currentColor"),
+ Class("w-5 h-5"),
+ Group(els),
+ )
+ })
+}
diff --git a/pkg/ui/layouts/auth.go b/pkg/ui/layouts/auth.go
new file mode 100644
index 0000000..f8491e7
--- /dev/null
+++ b/pkg/ui/layouts/auth.go
@@ -0,0 +1,40 @@
+package layouts
+
+import (
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func Auth(r *ui.Request, content Node) Node {
+ return Doctype(
+ HTML(
+ Lang("en"),
+ Data("theme", "dark"),
+ Head(
+ Metatags(r),
+ CSS(),
+ JS(),
+ ),
+ Body(
+ Div(
+ Class("hero flex items-center justify-center min-h-screen"),
+ Div(
+ Class("flex-col hero-content"),
+ Div(
+ Class("card shadow-md bg-base-200 w-96"),
+ Div(
+ Class("card-body"),
+ If(len(r.Title) > 0, H1(Class("text-2xl font-bold"), Text(r.Title))),
+ FlashMessages(r),
+ content,
+ ),
+ ),
+ ),
+ ),
+ HtmxListeners(r),
+ ),
+ ),
+ )
+}
diff --git a/pkg/ui/layouts/primary.go b/pkg/ui/layouts/primary.go
new file mode 100644
index 0000000..21f6e46
--- /dev/null
+++ b/pkg/ui/layouts/primary.go
@@ -0,0 +1,181 @@
+package layouts
+
+import (
+ "github.com/mikestefanello/pagoda/ent/admin"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ "github.com/mikestefanello/pagoda/pkg/ui/cache"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ "github.com/mikestefanello/pagoda/pkg/ui/icons"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func Primary(r *ui.Request, content Node) Node {
+ return Doctype(
+ HTML(
+ Lang("en"),
+ Data("theme", "dark"),
+ Head(
+ Metatags(r),
+ CSS(),
+ JS(),
+ ),
+ Body(
+ Div(
+ Class("drawer lg:drawer-open"),
+ Input(
+ ID("sidebar"),
+ Type("checkbox"),
+ Class("drawer-toggle"),
+ ),
+ Div(
+ Class("drawer-content flex flex-col p-7 prose-base"),
+ If(len(r.Title) > 0, H1(Text(r.Title))),
+ FlashMessages(r),
+ content,
+ Label(
+ For("sidebar"),
+ Class("btn btn-primary drawer-button lg:hidden"),
+ Text("Open drawer"),
+ ),
+ ),
+ sidebarMenu(r),
+ ),
+ searchModal(r),
+ HtmxListeners(r),
+ ),
+ ),
+ )
+}
+
+func search() Node {
+ return cache.SetIfNotExists("layout.search", func() Node {
+ return Div(
+ Class("ml-2"),
+ Attr("x-data", ""),
+ Label(
+ Class("input"),
+ icons.MagnifyingGlass(),
+ Input(
+ Type("search"),
+ Class("grow"),
+ Placeholder("Search"),
+ Attr("@click", "search_modal.showModal();"),
+ ),
+ ),
+
+ )
+ })
+}
+
+func searchModal(r *ui.Request) Node {
+ return cache.SetIfNotExists("layout.searchModal", func() Node {
+ return Dialog(
+ ID("search_modal"),
+ Class("modal"),
+ Div(
+ Class("modal-box"),
+ Form(
+ Method("dialog"),
+ Button(
+ Class("btn btn-sm btn-circle btn-ghost absolute right-2 top-2"),
+ Text("✕"),
+ ),
+ ),
+ H3(
+ Class("text-lg font-bold mb-2"),
+ Text("Search"),
+ ),
+ Input(
+ Attr("hx-get", r.Path(routenames.Search)),
+ Attr("hx-trigger", "keyup changed delay:500ms"),
+ Attr("hx-target", "#results"),
+ Name("query"),
+ Class("input w-full"),
+ Type("search"),
+ Placeholder("Search..."),
+ ),
+ Ul(
+ ID("results"),
+ Class("list"),
+ ),
+ ),
+ Form(
+ Method("dialog"),
+ Class("modal-backdrop"),
+ Button(
+ Text("close"),
+ ),
+ ),
+ )
+ })
+}
+
+func sidebarMenu(r *ui.Request) Node {
+ header := func(text string) Node {
+ return Li(
+ Class("menu-title mt-3 uppercase"),
+ Span(Text(text)),
+ )
+ }
+
+ adminSubMenu := func() Node {
+ entityTypeLinks := make(Group, len(admin.GetEntityTypes()))
+ for _, n := range admin.GetEntityTypes() {
+ entityTypeLinks = append(
+ entityTypeLinks,
+ MenuLink(r, icons.PencilSquare(), n.GetName(), routenames.AdminEntityList(n.GetName())),
+ )
+ }
+
+ return Group{
+ header("Entities"),
+ entityTypeLinks,
+ header("Monitoring"),
+ Li(
+ A(
+ icons.CircleStack(),
+ Href(r.Path(routenames.AdminTasks)),
+ Text("Tasks"),
+ Target("_blank"),
+ ),
+ ),
+ }
+ }
+
+ return Div(
+ Class("drawer-side"),
+ Label(
+ For("sidebar"),
+ Aria("label", "close sidebar"),
+ Class("drawer-overlay"),
+ ),
+ Div(
+ Class("menu bg-base-200 text-base-content min-h-full w-80 p-4"),
+ Div(
+ Class("w-2/3 mx-auto mt-3 mb-10"),
+ Img(
+ Src(ui.StaticFile("logo.png")),
+ ),
+ ),
+ search(),
+ Ul(
+ HxBoost(),
+ header("General"),
+ MenuLink(r, icons.Home(), "Dashboard", routenames.Home),
+ MenuLink(r, icons.Info(), "About", routenames.About),
+ MenuLink(r, icons.Mail(), "Contact", routenames.Contact),
+ MenuLink(r, icons.Archive(), "Cache", routenames.Cache),
+ MenuLink(r, icons.CircleStack(), "Task", routenames.Task),
+ MenuLink(r, icons.Document(), "Files", routenames.Files),
+ header("Account"),
+ If(r.IsAuth, MenuLink(r, icons.Exit(), "Logout", routenames.Logout)),
+ If(!r.IsAuth, MenuLink(r, icons.Enter(), "Login", routenames.Login)),
+ If(!r.IsAuth, MenuLink(r, icons.UserPlus(), "Register", routenames.Register)),
+ If(!r.IsAuth, MenuLink(r, icons.QuestionCircle(), "Forgot password", routenames.ForgotPasswordSubmit)),
+ Iff(r.IsAdmin, adminSubMenu),
+ ),
+ ),
+ )
+}
diff --git a/pkg/ui/models/file.go b/pkg/ui/models/file.go
new file mode 100644
index 0000000..257efcd
--- /dev/null
+++ b/pkg/ui/models/file.go
@@ -0,0 +1,22 @@
+package models
+
+import (
+ "fmt"
+
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type File struct {
+ Name string
+ Size int64
+ Modified string
+}
+
+func (f *File) Render() Node {
+ return Tr(
+ Td(Text(f.Name)),
+ Td(Text(fmt.Sprint(f.Size))),
+ Td(Text(f.Modified)),
+ )
+}
diff --git a/pkg/ui/models/post.go b/pkg/ui/models/post.go
new file mode 100644
index 0000000..67e0b2b
--- /dev/null
+++ b/pkg/ui/models/post.go
@@ -0,0 +1,67 @@
+package models
+
+import (
+ "fmt"
+
+ "github.com/mikestefanello/pagoda/pkg/pager"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type (
+ Posts struct {
+ Posts []Post
+ Pager pager.Pager
+ }
+
+ Post struct {
+ ID int
+ Title, Body string
+ }
+)
+
+func (p *Posts) Render(path string) Node {
+ g := make(Group, len(p.Posts))
+ for i, post := range p.Posts {
+ g[i] = post.Render()
+ }
+
+ return Div(
+ ID("posts"),
+ Ul(
+ Class("list bg-base-100 rounded-box shadow-md not-prose"),
+ g,
+ ),
+ Div(Class("mb-4")),
+ Pager(p.Pager.Page, path, !p.Pager.IsEnd(), "#posts"),
+ )
+}
+
+func (p *Post) Render() Node {
+ return Li(
+ Class("list-row"),
+ Div(
+ Class("text-4xl font-thin opacity-30 tabular-nums"),
+ Text(fmt.Sprintf("%02d", p.ID)),
+ ),
+ Div(
+ Img(
+ Class("size-10 rounded-box"),
+ Src(ui.StaticFile("gopher.png")),
+ Alt("Gopher"),
+ ),
+ ),
+ Div(
+ Class("list-col-grow"),
+ Div(
+ Text(p.Title),
+ ),
+ Div(
+ Class("text-xs font-semibold opacity-60"),
+ Text(p.Body),
+ ),
+ ),
+ )
+}
diff --git a/pkg/ui/models/search_result.go b/pkg/ui/models/search_result.go
new file mode 100644
index 0000000..0a831b4
--- /dev/null
+++ b/pkg/ui/models/search_result.go
@@ -0,0 +1,21 @@
+package models
+
+import (
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+type SearchResult struct {
+ Title string
+ URL string
+}
+
+func (s *SearchResult) Render() Node {
+ return Li(
+ Class("list-row"),
+ A(
+ Href(s.URL),
+ Text(s.Title),
+ ),
+ )
+}
diff --git a/pkg/ui/pages/about.go b/pkg/ui/pages/about.go
new file mode 100644
index 0000000..eab53aa
--- /dev/null
+++ b/pkg/ui/pages/about.go
@@ -0,0 +1,61 @@
+package pages
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ "github.com/mikestefanello/pagoda/pkg/ui/cache"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func About(ctx echo.Context) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "About"
+ r.Metatags.Description = "Learn a little about what's included in Pagoda."
+
+ // The tabs are static, so we can render and cache them.
+ tabs := cache.SetIfNotExists("pages.about.Tabs", func() Node {
+ return Group{
+ H2(Text("Frontend")),
+ P(Text("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.")),
+ Tabs(
+ []Tab{
+ {
+ Title: "HTMX",
+ Body: "Completes HTML as a hypertext by providing attributes to AJAXify anything and much more. Visit htmx.org to learn more.",
+ },
+ {
+ Title: "Alpine.js",
+ Body: "Drop-in, Vue-like functionality written directly in your markup. Visit alpinejs.dev to learn more.",
+ },
+ {
+ Title: "DaisyUI",
+ Body: "DaisyUI is the Tailwind CSS plugin you will love! It provides useful component class names to help you write less code and build faster. No JavaScript requirements. Visit daisyui.com to learn more.",
+ },
+ },
+ ),
+ H2(Text("Backend")),
+ P(Text("The following incredible projects provide the foundation of the Go backend. See the repository for a complete list of included projects.")),
+ Tabs(
+ []Tab{
+ {
+ Title: "Echo",
+ Body: "High performance, extensible, minimalist Go web framework. Visit echo.labstack.com to learn more.",
+ },
+ {
+ Title: "Ent",
+ Body: "Simple, yet powerful ORM for modeling and querying data. Visit entgo.io to learn more.",
+ },
+ {
+ Title: "Gomponents",
+ Body: "HTML components written in pure Go. They render to HTML 5, and make it easy for you to build reusable components. Visit gomponents.com to learn more.",
+ },
+ },
+ ),
+ }
+ })
+
+ return r.Render(layouts.Primary, tabs)
+}
diff --git a/pkg/ui/pages/admin_entity.go b/pkg/ui/pages/admin_entity.go
new file mode 100644
index 0000000..adefed2
--- /dev/null
+++ b/pkg/ui/pages/admin_entity.go
@@ -0,0 +1,115 @@
+package pages
+
+import (
+ "fmt"
+ "net/url"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/ent/admin"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func AdminEntityDelete(ctx echo.Context, entityType admin.EntityType) error {
+ r := ui.NewRequest(ctx)
+ r.Title = fmt.Sprintf("Delete %s", entityType.GetName())
+
+ return r.Render(
+ layouts.Primary,
+ forms.AdminEntityDelete(r, entityType),
+ )
+}
+
+func AdminEntityInput(ctx echo.Context, entityType admin.EntityType, values url.Values) error {
+ r := ui.NewRequest(ctx)
+ if values == nil {
+ r.Title = fmt.Sprintf("Add %s", entityType.GetName())
+ } else {
+ r.Title = fmt.Sprintf("Edit %s", entityType.GetName())
+ }
+
+ return r.Render(
+ layouts.Primary,
+ forms.AdminEntity(r, entityType, values),
+ )
+}
+
+func AdminEntityList(
+ ctx echo.Context,
+ entityType admin.EntityType,
+ entityList *admin.EntityList,
+) error {
+ r := ui.NewRequest(ctx)
+ r.Title = entityType.GetName()
+
+ genHeader := func() Node {
+ g := make(Group, 0, len(entityList.Columns)+2)
+ g = append(g, Th(Text("ID")))
+ for _, h := range entityList.Columns {
+ g = append(g, Th(Text(h)))
+ }
+ g = append(g, Th())
+ return g
+ }
+
+ genRow := func(row admin.EntityValues) Node {
+ g := make(Group, 0, len(row.Values)+3)
+ g = append(g, Th(Text(fmt.Sprint(row.ID))))
+ for _, h := range row.Values {
+ g = append(g, Td(Text(h)))
+ }
+ g = append(g,
+ Td(
+ ButtonLink(
+ ColorInfo,
+ r.Path(routenames.AdminEntityEdit(entityType.GetName()), row.ID),
+ "Edit",
+ ),
+ Span(Class("mr-2")),
+ ButtonLink(
+ ColorError,
+ r.Path(routenames.AdminEntityDelete(entityType.GetName()), row.ID),
+ "Delete",
+ ),
+ ),
+ )
+ return g
+ }
+
+ genRows := func() Node {
+ g := make(Group, 0, len(entityList.Entities))
+ for _, row := range entityList.Entities {
+ g = append(g, Tr(genRow(row)))
+ }
+ return g
+ }
+
+ return r.Render(layouts.Primary, Group{
+ Div(
+ Class("form-control mb-2"),
+ ButtonLink(
+ ColorAccent,
+ r.Path(routenames.AdminEntityAdd(entityType.GetName())),
+ fmt.Sprintf("Add %s", entityType.GetName()),
+ ),
+ ),
+ Table(
+ Class("table table-zebra mb-2"),
+ THead(
+ Tr(genHeader()),
+ ),
+ TBody(genRows()),
+ ),
+ Pager(
+ entityList.Page,
+ r.Path(routenames.AdminEntityAdd(entityType.GetName())),
+ entityList.HasNextPage,
+ "",
+ ),
+ })
+}
diff --git a/pkg/ui/pages/auth.go b/pkg/ui/pages/auth.go
new file mode 100644
index 0000000..aa27789
--- /dev/null
+++ b/pkg/ui/pages/auth.go
@@ -0,0 +1,46 @@
+package pages
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func Login(ctx echo.Context, form *forms.Login) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "Login"
+
+ return r.Render(layouts.Auth, form.Render(r))
+}
+
+func Register(ctx echo.Context, form *forms.Register) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "Register"
+
+ return r.Render(layouts.Auth, form.Render(r))
+}
+
+func ForgotPassword(ctx echo.Context, form *forms.ForgotPassword) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "Forgot password"
+
+ g := Group{
+ Div(
+ Class("content"),
+ P(Text("Enter your email address and we'll email you a link that allows you to reset your password.")),
+ ),
+ form.Render(r),
+ }
+
+ return r.Render(layouts.Auth, g)
+}
+
+func ResetPassword(ctx echo.Context, form *forms.ResetPassword) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "Reset your password"
+
+ return r.Render(layouts.Auth, form.Render(r))
+}
diff --git a/pkg/ui/pages/cache.go b/pkg/ui/pages/cache.go
new file mode 100644
index 0000000..ed57296
--- /dev/null
+++ b/pkg/ui/pages/cache.go
@@ -0,0 +1,15 @@
+package pages
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+)
+
+func UpdateCache(ctx echo.Context, form *forms.Cache) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "Set a cache entry"
+
+ return r.Render(layouts.Primary, form.Render(r))
+}
diff --git a/pkg/ui/pages/contact.go b/pkg/ui/pages/contact.go
new file mode 100644
index 0000000..66bb37a
--- /dev/null
+++ b/pkg/ui/pages/contact.go
@@ -0,0 +1,46 @@
+package pages
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func ContactUs(ctx echo.Context, form *forms.Contact) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "Contact us"
+ r.Metatags.Description = "Get in touch with us."
+
+ g := Group{
+ Iff(r.Htmx.Target != "contact", func() Node {
+ return Card(CardParams{
+ Title: "Card component",
+ Body: Group{
+ Span(Text("This is an example of a form with inline, server-side validation and HTMX-powered AJAX submissions without writing a single line of JavaScript.")),
+ Span(Text("Only the form below will update async upon submission.")),
+ },
+ Color: ColorWarning,
+ Size: SizeMedium,
+ })
+ }),
+ Iff(form.IsDone(), func() Node {
+ return Card(CardParams{
+ Title: "Thank you!",
+ Body: Group{
+ Span(Text("No email was actually sent but this entire operation was handled server-side and degrades without JavaScript enabled.")),
+ },
+ Color: ColorSuccess,
+ Size: SizeLarge,
+ })
+ }),
+ Iff(!form.IsDone(), func() Node {
+ return form.Render(r)
+ }),
+ }
+
+ return r.Render(layouts.Primary, g)
+}
diff --git a/pkg/ui/pages/error.go b/pkg/ui/pages/error.go
new file mode 100644
index 0000000..21d42cf
--- /dev/null
+++ b/pkg/ui/pages/error.go
@@ -0,0 +1,38 @@
+package pages
+
+import (
+ "net/http"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func Error(ctx echo.Context, code int) error {
+ r := ui.NewRequest(ctx)
+ r.Title = http.StatusText(code)
+ var body Node
+
+ switch code {
+ case http.StatusInternalServerError:
+ body = Text("Please try again.")
+ case http.StatusForbidden, http.StatusUnauthorized:
+ body = Text("You are not authorized to view the requested page.")
+ case http.StatusNotFound:
+ body = Group{
+ Text("Click "),
+ A(
+ Href(r.Path(routenames.Home)),
+ Text("here"),
+ ),
+ Text(" to go return home."),
+ }
+ default:
+ body = Text("Something went wrong.")
+ }
+
+ return r.Render(layouts.Primary, P(body))
+}
diff --git a/pkg/ui/pages/file.go b/pkg/ui/pages/file.go
new file mode 100644
index 0000000..e7029b2
--- /dev/null
+++ b/pkg/ui/pages/file.go
@@ -0,0 +1,53 @@
+package pages
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+ "github.com/mikestefanello/pagoda/pkg/ui/models"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func UploadFile(ctx echo.Context, files []*models.File) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "Upload a file"
+
+ fileList := make(Group, len(files))
+ for i, file := range files {
+ fileList[i] = file.Render()
+ }
+
+ n := Group{
+ P(Text("This is a very basic example of how to handle file uploads. Files uploaded will be saved to the directory specified in your configuration.")),
+ Divider(""),
+ forms.File{}.Render(r),
+ Divider(""),
+ H3(
+ Class("title"),
+ Text("Uploaded files"),
+ ),
+ Card(CardParams{
+ Body: Group{Text("Below are all files in the configured upload directory.")},
+ Color: ColorWarning,
+ Size: SizeMedium,
+ }),
+ Table(
+ Class("table"),
+ THead(
+ Tr(
+ Th(Text("Filename")),
+ Th(Text("Size")),
+ Th(Text("Modified on")),
+ ),
+ ),
+ TBody(
+ fileList,
+ ),
+ ),
+ }
+
+ return r.Render(layouts.Primary, n)
+}
diff --git a/pkg/ui/pages/home.go b/pkg/ui/pages/home.go
new file mode 100644
index 0000000..22a7fd3
--- /dev/null
+++ b/pkg/ui/pages/home.go
@@ -0,0 +1,107 @@
+package pages
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/routenames"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ "github.com/mikestefanello/pagoda/pkg/ui/icons"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+ "github.com/mikestefanello/pagoda/pkg/ui/models"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func Home(ctx echo.Context, posts *models.Posts) error {
+ r := ui.NewRequest(ctx)
+ r.Metatags.Description = "This is the home page."
+ r.Metatags.Keywords = []string{"Software", "Coding", "Go"}
+
+ // This pages helps to illustrate the different options you can take when using HTMX to introduce interactivity
+ // to your web application. The following three options are available, but here, we're opting for the first one.
+ // 1) Highly-optimized and progressive enhancement:
+ // This is highly-optimized because the server is doing the least amount of work possible, only rendering
+ // the least amount possible based on the incoming request. It's possible that even your route handler would
+ // want to check the HTMX request in order to limit what it does. With HTMX, it's possible to still return a
+ // normal, full page, but use hx-select to pluck out only the part you want to re-render. It requires some extra
+ // condition checks and code but performance is improved. Progressive enhancement refers to having a fully
+ // functional web app, even if JS was disabled, but providing the enhancement if JS is enabled. All of these
+ // examples should continue to work fine without JS.
+ // 2) Not optimized and progressive enhancement:
+ // As mentioned previously, you can remove all of these conditions, re-render the entire page for every request,
+ // and rely on HTMX's hx-select to only replace what you want to (ie, the posts).
+ // 3) Optimized and partial renderings:
+ // You could have a separate route that is only for fetching posts while paging, and that would render only
+ // that partial HTML, which HTMX would then use to inject in to this page.
+
+ headerMsg := func() Node {
+ return Group{
+ Stats(
+ Stat{
+ Title: "User name",
+ Value: func() string {
+ if r.IsAuth {
+ return r.AuthUser.Name
+ }
+ return "(not logged in)"
+ }(),
+ Description: "The logged in user's name",
+ Icon: icons.UserCircle(),
+ },
+ Stat{
+ Title: "Admin status",
+ Value: func() string {
+ if r.IsAdmin {
+ return "Administrator"
+ }
+ return "Non-administrator"
+ }(),
+ Description: "Use `make admin` to create an admin account",
+ Icon: icons.LockClosed(),
+ },
+ Stat{
+ Title: "GitHub Stars",
+ Value: "2,500+",
+ Description: "Star if you like Pagoda",
+ Icon: icons.Star(),
+ },
+ ),
+ H2(Text("Recent posts")),
+ Span(Text("Below is an example of both paging and AJAX fetching using HTMX")),
+ }
+ }
+
+ cards := func() Node {
+ return Div(
+ Class("flex w-full gap-2 mt-5"),
+ Card(CardParams{
+ Title: "Serving files",
+ Body: Group{
+ Text("In the example posts above, check how the file URL contains a cache-buster query parameter which changes only when the app is restarted. "),
+ Text("Static files also contain cache-control headers which are configured via middleware."),
+ },
+ Color: ColorWarning,
+ Size: SizeSmall,
+ }),
+ Card(CardParams{
+ Title: "Documentation",
+ Body: Group{
+ Text("Have you read through the entire documentation? If not, you may be missing functionality or have questions. "),
+ },
+ Footer: Group{
+ ButtonLink(ColorNeutral, "https://github.com/mikestefanello/pagoda?tab=readme-ov-file#table-of-contents", "Learn more"),
+ },
+ Color: ColorNeutral,
+ Size: SizeSmall,
+ }),
+ )
+ }
+
+ g := Group{
+ Iff(r.Htmx.Target != "posts", headerMsg),
+ posts.Render(r.Path(routenames.Home)),
+ Iff(r.Htmx.Target != "posts", cards),
+ }
+
+ return r.Render(layouts.Primary, g)
+}
diff --git a/pkg/ui/pages/search.go b/pkg/ui/pages/search.go
new file mode 100644
index 0000000..0af5cee
--- /dev/null
+++ b/pkg/ui/pages/search.go
@@ -0,0 +1,20 @@
+package pages
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+ "github.com/mikestefanello/pagoda/pkg/ui/models"
+ . "maragu.dev/gomponents"
+)
+
+func SearchResults(ctx echo.Context, results []*models.SearchResult) error {
+ r := ui.NewRequest(ctx)
+
+ g := make(Group, len(results))
+ for i, result := range results {
+ g[i] = result.Render()
+ }
+
+ return r.Render(layouts.Primary, g)
+}
diff --git a/pkg/ui/pages/task.go b/pkg/ui/pages/task.go
new file mode 100644
index 0000000..cbe5cff
--- /dev/null
+++ b/pkg/ui/pages/task.go
@@ -0,0 +1,41 @@
+package pages
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/pkg/ui"
+ . "github.com/mikestefanello/pagoda/pkg/ui/components"
+ "github.com/mikestefanello/pagoda/pkg/ui/forms"
+ "github.com/mikestefanello/pagoda/pkg/ui/layouts"
+ . "maragu.dev/gomponents"
+ . "maragu.dev/gomponents/html"
+)
+
+func AddTask(ctx echo.Context, form *forms.Task) error {
+ r := ui.NewRequest(ctx)
+ r.Title = "Create a task"
+ r.Metatags.Description = "Test creating a task to see how it works."
+
+ g := Group{
+ Iff(r.Htmx.Target != "task", func() Node {
+ return Group{
+ P(Raw("Submitting this form will create an ExampleTask in the task queue. After the specified delay, the message will be logged by the queue processor.")),
+ P(Raw("See pkg/tasks and the README for more information.")),
+ }
+ }),
+ form.Render(r),
+ Iff(r.Htmx.Target != "task", func() Node {
+ var text string
+ if r.IsAdmin {
+ text = "View all queued tasks by clicking on the Tasks link in the sidebar."
+ } else {
+ text = "Log in as an admin in order to access the task and queue monitoring UI."
+ }
+ return Group{
+ Div(Class("mt-5")),
+ Alert(ColorWarning, text),
+ }
+ }),
+ }
+
+ return r.Render(layouts.Primary, g)
+}
diff --git a/pkg/ui/request.go b/pkg/ui/request.go
new file mode 100644
index 0000000..db62898
--- /dev/null
+++ b/pkg/ui/request.go
@@ -0,0 +1,114 @@
+package ui
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/htmx"
+ "maragu.dev/gomponents"
+)
+
+type (
+ // Request encapsulates information about the incoming request in order to provide your ui with important and
+ // useful information needed for rendering.
+ Request struct {
+ // Title stores the title of the page.
+ Title string
+
+ // Context stores the request context.
+ Context echo.Context
+
+ // CurrentPath stores the path of the current request.
+ CurrentPath string
+
+ // IsHome stores whether the requested page is the home page.
+ IsHome bool
+
+ // IsAuth stores whether the user is authenticated.
+ IsAuth bool
+
+ // IsAdmin stores whether the user is an admin.
+ IsAdmin bool
+
+ // AuthUser stores the authenticated user.
+ AuthUser *ent.User
+
+ // Metatags stores metatag values.
+ Metatags struct {
+ // Description stores the description metatag value.
+ Description string
+
+ // Keywords stores the keywords metatag values.
+ Keywords []string
+ }
+
+ // 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
+
+ // Htmx stores information provided by HTMX about this request.
+ Htmx *htmx.Request
+
+ // Config stores the application configuration.
+ // This will only be populated if the Config middleware is installed in the router.
+ Config *config.Config
+ }
+
+ // LayoutFunc is a callback function intended to render your page node within a given layout.
+ // This is handled as a callback to automatically support HTMX requests so that you can respond
+ // with only the page content and not the entire layout.
+ // See Request.Render().
+ LayoutFunc func(*Request, gomponents.Node) gomponents.Node
+)
+
+// NewRequest generates a new Request using the Echo context of a given HTTP request.
+func NewRequest(ctx echo.Context) *Request {
+ p := &Request{
+ Context: ctx,
+ CurrentPath: ctx.Request().URL.Path,
+ Htmx: htmx.GetRequest(ctx),
+ }
+
+ p.IsHome = p.CurrentPath == "/"
+
+ if csrf := ctx.Get(context.CSRFKey); csrf != nil {
+ p.CSRF = csrf.(string)
+ }
+
+ if u := ctx.Get(context.AuthenticatedUserKey); u != nil {
+ p.IsAuth = true
+ p.AuthUser = u.(*ent.User)
+ p.IsAdmin = p.AuthUser.Admin
+ }
+
+ if cfg := ctx.Get(context.ConfigKey); cfg != nil {
+ p.Config = cfg.(*config.Config)
+ }
+
+ return p
+}
+
+// Path generates a URL path for a given route name and optional route parameters.
+// This will only work if you've supplied names for each of your routes. It's optional to use and helps avoids
+// having duplicate, hard-coded paths and parameters all over your application.
+func (r *Request) Path(routeName string, routeParams ...any) string {
+ return r.Context.Echo().Reverse(routeName, routeParams...)
+}
+
+// Url generates an absolute URL for a given route name and optional route parameters.
+func (r *Request) Url(routeName string, routeParams ...any) string {
+ return r.Config.App.Host + r.Path(routeName, routeParams...)
+}
+
+// Render renders a given node, optionally within a given layout based on the HTMX request headers.
+// If the request is being made by HTMX and is not boosted, this will automatically only render the node without
+// the layout, to support partial rendering.
+func (r *Request) Render(layout LayoutFunc, node gomponents.Node) error {
+ if r.Htmx.Enabled && !r.Htmx.Boosted {
+ return node.Render(r.Context.Response().Writer)
+ }
+
+ return layout(r, node).Render(r.Context.Response().Writer)
+}
diff --git a/pkg/ui/request_test.go b/pkg/ui/request_test.go
new file mode 100644
index 0000000..e8a4987
--- /dev/null
+++ b/pkg/ui/request_test.go
@@ -0,0 +1,93 @@
+package ui
+
+import (
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/mikestefanello/pagoda/config"
+ "github.com/mikestefanello/pagoda/ent"
+ "github.com/mikestefanello/pagoda/pkg/context"
+ "github.com/mikestefanello/pagoda/pkg/htmx"
+ "github.com/mikestefanello/pagoda/pkg/tests"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "maragu.dev/gomponents"
+ "maragu.dev/gomponents/html"
+)
+
+func TestNewRequest(t *testing.T) {
+ e := echo.New()
+ ctx, _ := tests.NewContext(e, "/")
+ r := NewRequest(ctx)
+ assert.Same(t, ctx, r.Context)
+ assert.Equal(t, "/", r.CurrentPath)
+ assert.True(t, r.IsHome)
+ assert.False(t, r.IsAuth)
+ assert.Nil(t, r.AuthUser)
+ assert.Empty(t, r.CSRF)
+ assert.Nil(t, r.Config)
+ assert.Same(t, htmx.GetRequest(ctx), r.Htmx)
+
+ ctx, _ = tests.NewContext(e, "/abc")
+ usr := &ent.User{
+ ID: 1,
+ }
+ ctx.Set(context.AuthenticatedUserKey, usr)
+ ctx.Set(context.CSRFKey, "12345")
+ ctx.Set(context.ConfigKey, &config.Config{
+ App: config.AppConfig{
+ Name: "testing",
+ },
+ })
+ r = NewRequest(ctx)
+ assert.Equal(t, "/abc", r.CurrentPath)
+ assert.False(t, r.IsHome)
+ assert.True(t, r.IsAuth)
+ assert.Equal(t, usr, r.AuthUser)
+ assert.Equal(t, "12345", r.CSRF)
+ assert.Equal(t, "testing", r.Config.App.Name)
+}
+
+func TestRequest_UrlPath(t *testing.T) {
+ e := echo.New()
+ e.GET("/abc/:id", func(c echo.Context) error { return nil }).Name = "test"
+ ctx, _ := tests.NewContext(e, "/")
+ r := NewRequest(ctx)
+ r.Config = &config.Config{
+ App: config.AppConfig{
+ Host: "http://localhost",
+ },
+ }
+
+ assert.Equal(t, "http://localhost/abc/123", r.Url("test", 123))
+ assert.Equal(t, "/abc/123", r.Path("test", 123))
+}
+
+func TestRequest_Render(t *testing.T) {
+ e := echo.New()
+ layout := func(r *Request, n gomponents.Node) gomponents.Node {
+ return html.Div(html.Class("test"), n)
+ }
+ node := html.P(gomponents.Text("hello"))
+
+ t.Run("no htmx", func(t *testing.T) {
+ ctx, rec := tests.NewContext(e, "/")
+ r := NewRequest(ctx)
+ r.Htmx = &htmx.Request{}
+ err := r.Render(layout, node)
+ require.NoError(t, err)
+ assert.Equal(t, `
-{{end}}
\ No newline at end of file
diff --git a/templates/pages/about.gohtml b/templates/pages/about.gohtml
deleted file mode 100644
index 06524c2..0000000
--- a/templates/pages/about.gohtml
+++ /dev/null
@@ -1,41 +0,0 @@
-{{define "content"}}
- {{- if .Data.FrontendTabs}}
-
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.
{{if .IsAuth}}Welcome back!{{else}}Please login in to your account.{{end}}
-
-
-
-
-
-
Recent posts
-
- Below is an example of both paging and AJAX fetching using HTMX
-
-
-{{end}}
-
-{{define "posts"}}
-
- {{- range .Data}}
-
-
-
-
-
-
-
-
-
- {{.Title}}
-
- {{.Body}}
-
-
-
-
- {{- end}}
-
-
- {{- if not $.Pager.IsBeginning}}
-
-
-
- {{- end}}
- {{- if not $.Pager.IsEnd}}
-
-
-
- {{- end}}
-
-
-{{end}}
-
-{{define "file-msg"}}
-
-
-
-
Serving files
-
-
-
- In the example posts above, check how the file URL contains a cache-buster query parameter which changes only when the app is restarted.
- Static files also contain cache-control headers which are configured via middleware.
- You can also use AlpineJS to dismiss this message.
-
-
-{{end}}
\ No newline at end of file
diff --git a/templates/pages/login.gohtml b/templates/pages/login.gohtml
deleted file mode 100644
index 3d1a2a8..0000000
--- a/templates/pages/login.gohtml
+++ /dev/null
@@ -1,28 +0,0 @@
-{{define "content"}}
-
-{{end}}
\ No newline at end of file
diff --git a/templates/pages/register.gohtml b/templates/pages/register.gohtml
deleted file mode 100644
index 4455e9e..0000000
--- a/templates/pages/register.gohtml
+++ /dev/null
@@ -1,41 +0,0 @@
-{{define "content"}}
-
-{{end}}
\ No newline at end of file
diff --git a/templates/pages/reset-password.gohtml b/templates/pages/reset-password.gohtml
deleted file mode 100644
index 93a469b..0000000
--- a/templates/pages/reset-password.gohtml
+++ /dev/null
@@ -1,24 +0,0 @@
-{{define "content"}}
-
-{{end}}
\ No newline at end of file
diff --git a/templates/pages/search.gohtml b/templates/pages/search.gohtml
deleted file mode 100644
index dc32fb6..0000000
--- a/templates/pages/search.gohtml
+++ /dev/null
@@ -1,5 +0,0 @@
-{{define "content"}}
- {{- range .Data}}
- {{.Title}}
- {{- end}}
-{{end}}
\ No newline at end of file