Compare commits

...

153 commits

Author SHA1 Message Date
c72257132c adding gitignore
Some checks are pending
Test / test (push) Waiting to run
2026-05-20 08:01:33 +00:00
4d5d45d1a4 Updating to latest pagoda release 2026-05-20 08:00:26 +00:00
mikestefanello
05cf6c8318 Update dependencies. 2025-12-14 10:10:03 -05:00
mikestefanello
9aa6784751 Re-generate ent code. 2025-08-15 09:58:39 -04:00
mikestefanello
2a853c05d6 Update dependencies. 2025-08-15 09:56:50 -04:00
mikestefanello
a8a9f5ee3a Removed accidental user field inclusin. 2025-08-15 09:52:37 -04:00
mikestefanello
be5c1f9197 Fix enums in generated admin schema code. 2025-08-15 09:51:06 -04:00
mikestefanello
3ffdea90c3 Update dependencies. 2025-08-04 08:38:30 -04:00
Mike Stefanello
9e6d9fd063 Generate ent schema in admin code. (#127) 2025-08-04 08:32:10 -04:00
mikestefanello
67a97832a5 Support multiple forms on a single page. 2025-07-20 14:55:29 -04:00
mikestefanello
7505598324 Updated dependencies. 2025-07-18 09:01:48 -04:00
mikestefanello
96ad22cda3 Updated README. 2025-07-18 08:59:09 -04:00
Paulo Chaves
eba3d5e105 Set tailwindcss binary file name depending on current os (#125)
* Download tailwindcss arm64 binary if on macOS

* add tailwind binary download for macos x64 or arm64
2025-07-18 08:51:44 -04:00
mikestefanello
939e1ceed5 Fixed missing name attribute on select component. 2025-06-24 19:11:23 -04:00
Mike Stefanello
c1e9baabe6 Swap Bulma for DaisyUI (Tailwind) (#111) 2025-06-17 20:19:58 -04:00
mikestefanello
fc5db0e95a Fix Safari cookies and CSRF. 2025-06-02 08:42:14 -04:00
Eduardo Zepeda
9e15bceace Fix HTTPErrorHandler always returning 200 (#109) 2025-05-19 13:13:22 -04:00
mikestefanello
3cfcb43031 Added backlite UI to the admin panel. 2025-05-02 10:18:15 -04:00
mikestefanello
52f87580a0 Use memdb for in-memory sqlite dbs. 2025-04-22 09:09:56 -04:00
mikestefanello
a53bdf9a1b Cleanup configuration. 2025-04-22 08:54:56 -04:00
mikestefanello
77cabe5f12 Update dependencies. 2025-04-22 08:35:29 -04:00
Mike Stefanello
1a6874fd82 Add dynamic admin panel for managing entities (#108) 2025-04-22 08:26:35 -04:00
mikestefanello
60009df0bf Updated dependencies. 2025-04-10 09:11:44 -04:00
mikestefanello
4d7b780087 Fixed HTMX listeners. 2025-04-01 10:23:32 -04:00
mikestefanello
c8db468292 Cleaned up conditional rendering. 2025-03-29 09:40:04 -04:00
mikestefanello
02236266f1 Log ui node cache failures. 2025-03-25 09:25:18 -04:00
mikestefanello
83127d590a Update dependencies. 2025-03-16 19:37:14 -04:00
Mike Stefanello
051d032038 Migrate from templates to Gomponents (#103) 2025-03-05 20:01:58 -05:00
mikestefanello
0bf9ab7189 Upgraded ent. 2025-02-18 19:27:00 -05:00
mikestefanello
acbc5e4bf6 Upgrade to Bulma 1.0.2. 2025-02-18 19:19:09 -05:00
mikestefanello
3eab2f5562 Added file management. 2025-02-16 14:23:52 -05:00
mikestefanello
09b8393c8a Use custom logger in main. 2025-02-16 09:47:15 -05:00
mikestefanello
732e81f5a4 Move web/task shutdown to container. Shutdown web before tasks. Add shutdown timeouts to config. 2025-02-16 09:39:29 -05:00
mikestefanello
4e5c5127e8 Start task runner before web server. 2025-02-12 22:01:46 -05:00
mikestefanello
f8192d3ad6 Use more secure cookie settings. 2025-02-12 22:01:30 -05:00
mikestefanello
575c46db22 Updated test workflow go version. 2025-02-12 22:01:10 -05:00
mikestefanello
ca0743d8a6 Update dependencies. 2025-01-19 09:37:58 -05:00
Nick Krecklow
eee36e3cbe use alpine v3 click.outside event name (#83)
Alpine v3 deprecates the v2 `click.away` event and marks it for possible future removal, replacing it with `click.outside`.

See v3 release notes: https://alpinejs.dev/upgrade-guide#away-replace-with-outside
2024-10-02 10:32:25 -04:00
Dimitri Balios
42a07787f0 Update Makefile (#80)
ent install -d flag is deprecated. -d=true is a no-op
2024-09-18 17:06:54 -04:00
mikestefanello
e4370e8ba0 Fixed pager page and pages value when no items present. 2024-09-13 09:16:20 -04:00
mikestefanello
5a6cec5294 Switched to backlite for task queues. 2024-07-24 21:04:32 -04:00
mikestefanello
bce426dd0a Updated documentation. 2024-06-24 19:03:42 -04:00
mikestefanello
12c5662cb9 Upgrade to HTMX 2.0. 2024-06-22 11:07:43 -04:00
mikestefanello
be997e53d8 Fixed doc code formatting. 2024-06-22 10:38:57 -04:00
Mike Stefanello
a096abd195 Default to SQLite rather than Postgres & Redis (#72)
* Initial rough draft switch to sqlite.

* Rewrote cache implemenation.

* Provide typed tasks.

* Task cleanup.

* Use same db for tasks.

* Provide task queue registration and service container injection.

* Added optional delay to tasks. Pool buffers when encoding.

* Added tests for the task client and runner.

* Added handler examples for caching and tasks.

* Cleanup and documentation.

* Use make in workflow.

* Updated documentation.

* Updated documentation.
2024-06-22 10:34:26 -04:00
mikestefanello
5e9e502b42 Log request URI rather than path. 2024-06-19 09:32:22 -04:00
mikestefanello
a70003d290 Added redirect package. 2024-06-16 11:30:11 -04:00
mikestefanello
8cae6e6beb Added redirect support with query params. 2024-06-16 10:53:43 -04:00
mikestefanello
75aefa8a0a Removed echo-contrib dependency. 2024-06-15 16:56:47 -04:00
Mike Stefanello
8eafb6b666 Move controller to the template renderer (#68) 2024-06-15 15:34:24 -04:00
mikestefanello
baa391fb20 Misc cleanup. 2024-06-15 09:09:36 -04:00
Mike Stefanello
c8a3d64918 Replace Echo logger with slog. (#67)
* Replace Echo logger with slog.
2024-06-14 21:01:48 -04:00
Mike Stefanello
97bef0257e Improve form and template usage (#66)
* Improve form and template usage.
2024-06-14 12:35:35 -04:00
mikestefanello
5f66b0ee71 Updated documentation. 2024-06-10 08:28:00 -04:00
mikestefanello
26234c29a8 Fixed typo. 2024-06-09 21:42:43 -04:00
mikestefanello
4540276472 Easier form and form submission handling. 2024-06-09 21:39:04 -04:00
Mike Stefanello
ad4818aa8b Merge pull request #65 from mikestefanello/handlers-3
Switch from routes to self-registering handlers to group related routes.
2024-06-09 19:57:04 -04:00
mikestefanello
59c10f1874 Switch from routes to self-registering handlers to group related routes. 2024-06-09 12:31:30 -04:00
mikestefanello
8ca11c90e1 Added radio form element example. 2024-05-18 16:19:27 -04:00
mikestefanello
da7044654c Updated actions go version. 2024-04-27 16:26:50 -04:00
mikestefanello
9213eb88c6 Update dependencies. 2024-04-27 16:23:44 -04:00
Mike Stefanello
f3a95b4be7 Merge pull request #60 from jordanstephens/patch-1
Fix a broken link
2024-04-01 08:54:28 -04:00
Jordan Stephens
6e6d2e3f09 Fix a broken link 2024-03-30 18:47:33 -07:00
Mike Stefanello
4488b2255a Merge pull request #58 from saurori/cache-up
Update Cache related go modules go-redis to v9 and gocache to v4
2024-03-21 19:58:44 -04:00
Stefan Aurori
f718a6b798 Rename lib_store and redis_store imports. Cache get remove redundant nil check. 2024-03-14 20:43:27 -04:00
Stefan Aurori
472dc0c358 Update go-redis to v9 and gocache to v4 2024-03-10 17:23:03 -04:00
mikestefanello
740aebd1a9 Update crypto library. 2023-12-18 21:02:30 -05:00
mikestefanello
203a856895 Move htmx template into layouts. 2023-12-18 20:59:40 -05:00
mikestefanello
60c8aefd49 Use consts for route names and templates. 2023-12-16 11:07:20 -05:00
mikestefanello
a787d5dc7f Added additional make commands. 2023-12-16 08:12:51 -05:00
mikestefanello
5f877c3d38 Restore local live reloading of templates. 2023-12-12 20:07:58 -05:00
mikestefanello
29fbadbadd Replace template dir file path hack with embed directive. 2023-12-10 09:33:34 -05:00
mikestefanello
11f5ddcee4 Fix flaky context error test. 2023-11-15 20:14:00 -05:00
Mike Stefanello
524d1a4915 Merge pull request #48 from gedw99/main
fixes Issue 45 - cant generate using ent and then run
2023-11-15 20:06:58 -05:00
ged wed
ff82ba532a fixes 45 - cant generate 2023-11-14 12:46:57 +01:00
Mike Stefanello
ed327dc066 Merge pull request #44 from mips171/mips171/bump_bulma_htmx
bump Bulma and HTMX
2023-10-01 18:52:36 -04:00
Nicholas Smith
9c0f0087d2 bump Bulma and HTMX 2023-10-01 13:23:42 +10:00
mikestefanello
a75b07485c Merge branch 'main' of github.com:mikestefanello/pagoda into main 2023-09-17 11:38:26 -04:00
mikestefanello
2618920e85 Pin PG version to 15 to avoid broken schema migrations. 2023-09-17 11:38:10 -04:00
Mike Stefanello
ef5aecc4b0 Merge pull request #35 from testwill/ioutil
chore: remove refs to deprecated io/ioutil
2023-09-17 11:18:38 -04:00
guoguangwu
d815040397 chore: remove refs to deprecated io/ioutil 2023-07-26 11:08:24 +08:00
Mike Stefanello
bc917d4788 Merge pull request #33 from Jimmy99/patch-2
Update README.md
2023-05-25 15:55:50 -04:00
Dimitri Balios
3aad9e5efb Update README.md
Fix typo
2023-05-25 21:50:20 +02:00
Mike Stefanello
aed537d84e Merge pull request #32 from Jimmy99/patch-1
Update README.md for typo
2023-05-22 18:13:57 -04:00
Dimitri Balios
180b237b05 Update README.md for typo
Typo fix "faciliate" to "facilitate"
2023-05-22 23:10:28 +02:00
mikestefanello
bdbef15593 Update dependencies. 2023-02-25 07:49:58 -05:00
Mike Stefanello
b7de694716 Merge pull request #28 from Jimmy99/main
Update Makefile to fix deprecated ent init command
2023-02-25 07:47:55 -05:00
Dimitri Balios
bb880ef091 Update Makefile 2023-02-23 10:35:33 +02:00
Mike Stefanello
50ea32fe70 Merge pull request #26 from joshlemer/docker-compose-bind-localhost
Bind DB and Redis to 127.0.0.1
2023-02-05 13:05:48 -05:00
Josh Lemer
7bb28427d6 Bind DB and Redis to 127.0.0.1 2023-01-25 14:49:13 -08:00
Mike Stefanello
e676c48458 Merge pull request #25 from migeorge/update-test-workflow
Update Test Workflow
2023-01-18 09:01:42 -05:00
Mike George
d052aba309 move to v3 actions 2023-01-17 22:35:14 -05:00
Mike Stefanello
c90ffac0e8 Merge pull request #24 from mips171/mips171/makefile
Makefile fix to run worker
2023-01-05 20:32:31 -05:00
mips171
b03beccfb2 Update Makefile 2023-01-06 10:53:34 +10:00
Nicholas Smith
7e4d92fc92 feedback 2023-01-05 08:01:50 +10:00
Nicholas Smith
1f947eae7a regen entities on reset 2023-01-04 22:03:15 +10:00
Nicholas Smith
fd0487abac add make down 2023-01-04 22:02:53 +10:00
Nicholas Smith
70f96b4735 fix worker target 2023-01-04 22:02:05 +10:00
mikestefanello
76ccf3b8db Fixed docker make targets. 2022-11-04 18:49:25 -04:00
mikestefanello
bf8df2624b Bump Go version. 2022-11-02 19:37:07 -04:00
mikestefanello
2a69237f17 Bump Go version. 2022-11-02 19:34:40 -04:00
mikestefanello
071ede73dd Updated dependencies. 2022-11-02 19:27:42 -04:00
mikestefanello
dceb232cb2 Reorganized directories and packages. 2022-11-02 19:23:26 -04:00
mikestefanello
965fb540c7 Switch to viper for config management. 2022-11-02 14:50:19 -04:00
Mike Stefanello
99a7ec8a9e Merge pull request #14 from ahashim/dockerizing-db-and-cache
Dockerizing db and cache
2022-10-10 19:53:55 -04:00
Mike Stefanello
b33ef2e8d7 Merge pull request #15 from hbd/patch-1
fix: typo when logging fatal error
2022-10-10 13:41:46 -04:00
Zak
3c6bff3a3d fix: typo when logging fatal error
Small fix to format the error instead of printing `v`
2022-10-10 08:49:11 -07:00
Ahmed Hashim
521fea2530 docs: only removing the psql and redis-cli dependencies from README.md + trailing whitespace characters 2022-10-09 08:57:43 -04:00
Ahmed Hashim
9dc83a65cf docs: removing the psql and redis-cli dependencies from README.md because they are now provided through their docker containers 2022-10-09 08:13:34 -04:00
Ahmed Hashim
b4621afbd9 feat: connect to redis-cli from within docker container instead of users localhost 2022-10-09 08:08:32 -04:00
Ahmed Hashim
ac0adde9e6 feat: connect to psql from within docker container instead of users localhost 2022-10-09 08:07:12 -04:00
Ahmed Hashim
ea1ee27b89 feat: updgrading docker compose command to support the latest version 2022-10-09 08:06:14 -04:00
mikestefanello
11e0514d33 Updated echo. 2022-10-01 21:32:14 -04:00
mikestefanello
2c5d7039d3 Updated dependencies. 2022-08-28 10:18:47 -04:00
mikestefanello
bfe56d7a31 Fix incorrect params when building controller redirect route url. 2022-07-08 09:58:42 -04:00
Mike Stefanello
c0a7c9e3b1 Merge pull request #11 from arrkiin/dont_cache_auth
Prevent caching pages if user is authenticated
2022-06-21 09:49:11 -04:00
Jens
d6748ed1db Prevent caching pages if user is authenticated 2022-06-20 21:47:07 +02:00
mikestefanello
02d306fae3 Updated yaml.v3. 2022-05-29 09:15:29 -04:00
mikestefanello
77e9b60cbf Updated dependencies. 2022-05-29 09:08:57 -04:00
mikestefanello
7c8ecc2ebe Fixed potential test failures due to timing. 2022-05-17 08:57:43 -04:00
mikestefanello
ecd0120920 Let error handler handle all error logic, logging, and canceling. 2022-05-17 08:45:18 -04:00
mikestefanello
31a3503021 Fixed redirect status code. 2022-04-12 21:08:00 -04:00
mikestefanello
b4b8153d06 Fix auth token expiration test failures. 2022-04-12 21:05:19 -04:00
mikestefanello
24ae49b54f Allow HTMX to handle redirects if the request is boosted. 2022-04-12 20:56:00 -04:00
mikestefanello
a6289fe4cc Upgraded libraries. 2022-03-08 19:35:55 -05:00
mikestefanello
271c0e9699 README update. 2022-03-08 19:30:37 -05:00
mikestefanello
1809b154e3 Lint check adjustments. 2022-02-10 08:56:07 -05:00
mikestefanello
0cb52b6e12 Added make command to check for dependency updates. 2022-02-10 08:21:07 -05:00
mikestefanello
cb58b89b6c Fixed missing port in database connection string. 2022-02-08 20:21:05 -05:00
mikestefanello
90408d68a7 Added task worker service and example task processor. 2022-02-08 08:58:22 -05:00
mikestefanello
27a2389e2c Added make target to connect to the test cache. 2022-02-07 21:23:09 -05:00
mikestefanello
5def458946 Use a separate cache db when running tests. 2022-02-06 10:07:25 -05:00
mikestefanello
a8bd9f8b2d Added asynq and a task client to the container to faciliate task queues. 2022-02-02 21:24:52 -05:00
mikestefanello
eb1e42bb02 Updated echo and ent. 2022-01-28 08:45:16 -05:00
mikestefanello
f4c98ba523 Include password token entity ID in reset URL in order to prevent loading all tokens. 2022-01-27 08:44:12 -05:00
mikestefanello
5c64cd6191 Support CSRF on route test POST requests. 2022-01-27 07:54:05 -05:00
mikestefanello
e90434edd5 Fixed assert package import. 2022-01-20 18:02:14 -05:00
mikestefanello
22cece2d01 Simplified template renderer parsing and execution. 2022-01-19 09:14:18 -05:00
mikestefanello
e8d73421aa Expanded mail client for easier email operations. 2022-01-14 15:42:32 -05:00
mikestefanello
3f053711ba Remove need for slice when setting cache tags. Require cache key for get/set ops. 2022-01-14 13:07:19 -05:00
mikestefanello
bfbb9669aa Added custom cache client for much easier cache operations. 2022-01-13 21:13:41 -05:00
mikestefanello
d412e06dad Use entity update method rather than query. Fixed password reset form action. 2022-01-09 21:30:09 -05:00
mikestefanello
acd38c8205 Handle context cancellations and avoid logged errors. 2022-01-09 00:23:26 -05:00
mikestefanello
0f7da0864e Type message typo. 2022-01-08 15:35:02 -05:00
mikestefanello
ea46a38f68 Added user email verification support. 2022-01-08 15:32:18 -05:00
mikestefanello
feb11bbe5b Replaced hard-coded search path with URL generator. 2022-01-06 21:40:04 -05:00
mikestefanello
3f123cd69e Added encryption info to README. 2022-01-04 20:10:10 -05:00
mikestefanello
95cf930720 Added route errors to README. 2022-01-01 13:44:09 -05:00
mikestefanello
a986686247 Changed module name. 2022-01-01 10:44:18 -05:00
mikestefanello
f3545473af Updated README. 2022-01-01 10:40:10 -05:00
mikestefanello
970769c57f Updated README. Set project name. 2021-12-31 17:26:42 -05:00
mikestefanello
849bad562e Merge branch 'main' of github.com:mikestefanello/goweb into main 2021-12-31 10:09:57 -05:00
Mike Stefanello
119640ad5b Create test.yml 2021-12-31 10:02:56 -05:00
209 changed files with 10816 additions and 8579 deletions

52
.air.toml Normal file
View file

@ -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

31
.github/workflows/test.yml vendored Normal file
View file

@ -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

5
.gitignore vendored
View file

@ -1 +1,6 @@
.idea
dbs
uploads
tmp
tailwindcss
daisyui*.env

19
Dockerfile Normal file
View file

@ -0,0 +1,19 @@
# Stage 1: Build the Go binary
FROM golang:1.26-alpine AS builder
RUN apk add --no-cache gcc musl-dev # for go-sqlite3 (CGO)
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 go build -o /app/server ./cmd/web
# Stage 2: Minimal runtime image
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/server /app/server
COPY --from=builder /build/config /app/config
COPY --from=builder /build/public /app/public
RUN mkdir -p /app/dbs /app/uploads
EXPOSE 8000
CMD ["/app/server"]

104
Makefile
View file

@ -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 ./...
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

923
README.md
View file

@ -1,922 +1,11 @@
## (NAME) - Rapid, easy full-stack web development starter kit in Go
# personal-site
## Table of Contents
* [Introduction](#introduction)
* [Overview](#overview)
* [Motivation](#motivation)
* [Foundation](#foundation)
* [Backend](#backend)
* [Frontend](#frontend)
* [Storage](#storage)
* [Screenshots](#screenshots)
* [Getting started](#getting-started)
* [Dependencies](#dependencies)
* [Start the application](#start-the-application)
* [Running tests](#running-tests)
* [Clients](#clients)
* [Service container](#service-container)
* [Dependency injection](#dependency-injection)
* [Test dependencies](#test-dependencies)
* [Configuration](#configuration)
* [Environment overrides](#environment-overrides)
* [Environments](#environments)
* [Database](#database)
* [Auto-migrations](#auto-migrations)
* [Separate test database](#separate-test-database)
* [ORM](#orm)
* [Entity types](#entity-types)
* [New entity type](#new-entity-type)
* [Sessions](#sessions)
* [Authentication](#authentication)
* [Login / Logout](#login--logout)
* [Forgot password](#forgot-password)
* [Registration](#registration)
* [Authenticated user](#authenticated-user)
* [Middleware](#middleware)
* [Routes](#routes)
* [Custom middleware](#custom-middleware)
* [Controller / Dependencies](#controller--dependencies)
* [Patterns](#patterns)
* [Custom middleware](#custom-middleware)
* [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)
* [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)
* [Cache](#cache)
* [Static files](#static-files)
* [Cache control headers](#cache-control-headers)
* [Cache-buster](#cache-buster)
* [Email](#email)
* [HTTPS](#https)
* [Logging](#logging)
* [Roadmap](#roadmap)
* [Credits](#credits)
My personal site, built on Pagoda (https://github.com/mikestefanello/pagoda).
## Introduction
## Deploy
### Overview
See `docker-compose.yml` and `Dockerfile` for the production setup.
(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.
## License
todo
### Motivation
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
### 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.
#### 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.
#### Frontend
Go server-side rendered HTML combined with the projects below enable you to create slick, modern UIs without writing any JavaScript or CSS.
- [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.
#### 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.
### Screenshots
todo
## Getting started
### Dependencies
Ensure the following are 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)_
### 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`.
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`.
If you ever want to quickly drop the Docker containers and restart them in order to wipe all data, execute `make reset`.
### Running tests
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.
### 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
## 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:
- Configuration
- Cache
- Database
- ORM
- Web
- Validator
- Authentication
- Mail
- Template renderer
A new container can be created and initialized via `services.NewContainer()`. It can be later shutdown via `Shutdown()`.
### 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.
### Test dependencies
It is common that your tests will require access to dependencies, like the database, or any of the other services available within the container. Keeping all services in a container makes it especially easy to initialize everything within your tests. You can see an example pattern for doing this [here](#environments).
## 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.
Be sure to review and adjust all of the default configuration values provided.
### 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:
```go
Port uint16 `env:"HTTP_PORT,default=8000"`
```
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.
### Environments
The configuration value for the current _environment_ (`Config.App.Environment`) is an important one as it can influence some behavior significantly (will be explained in later sections).
A helper function (`config.SwitchEnvironment`) is available to make switching the environment easy, but this must be executed prior to loading the configuration. The common use-case for this is to switch the environment to `Test` before tests are executed:
```go
func TestMain(m *testing.M) {
// 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)
}
}()
// Run tests
exitVal := m.Run()
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`.
Database configuration can be found and managed within the `config` package.
### Auto-migrations
[Ent](https://entgo.io/) provides automatic migrations which are executed on the database whenever the `Container` is created, which means they will run when the application starts.
### 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`.
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.
## 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).
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.
### Entity types
The two included entity types are:
- User
- PasswordToken
### New entity type
While you should refer to their [documentation](https://entgo.io/docs/getting-started) for detailed usage, it's helpful to understand how to create an entity type and generate code. To make this easier, the `Makefile` contains some helpers.
1. Ensure all Ent code is downloaded by executing `make ent-install`.
2. Create the new entity type by executing `make ent-new name=User` where `User` is the name of the entity type. This will generate a file like you can see in `ent/schema/user.go` though the `Fields()` and `Edges()` will be left empty.
3. Populate the `Fields()` and optionally the `Edges()` (which are the relationships to other entity types).
4. When done, generate all code by executing `make ent-gen`.
The generated code is extremely flexible and impressive. An example to highlight this is one used within this application:
```go
entity, err := c.ORM.PasswordToken.
Query().
Where(passwordtoken.HasUserWith(user.ID(userID))).
Where(passwordtoken.CreatedAtGTE(expiration)).
All(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.
## 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.
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)
if err != nil {
return err
}
sess.Values["hello"] = "world"
sess.Values["isSomething"] = true
return sess.Save(ctx.Request(), ctx.Response())
}
```
## 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.
Authentication currently requires [sessions](#sessions) and the session middleware.
### Login / Logout
The `AuthClient` has methods `Login()` and `Logout()` to log a user in or out. To track a user's authentication state, data is stored in the session including the user ID and authentication status.
Prior to logging a user in, the method `CheckPassword()` can be used to determine if a user's password matches the hash stored in the database and on the `User` entity.
Routes are provided for the user to login and logout at `user/login` and `user/logout`.
### Forgot password
Users can reset their password in a secure manner by issuing a new password token via the method `GeneratePasswordResetToken()`. This creates a new `PasswordToken` entity in the database belonging to the user. The actual token itself, however, is not stored in the database for security purposes. It is only returned via the method so it can be used to build the reset URL for the email. Rather, a hash of the token is stored, using `bcrypt` the same package used to hash user passwords. The reason for doing this is the same as passwords. You do not want to store a plain-text value in the database that can be used to access an account.
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.
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`.
### Registration
The actual registration of a user is not handled within the `AuthClient` but rather just by creating a `User` entity. When creating a user, use `HashPassword()` to create a hash of the user's password, which is what will be stored in the database.
A route is provided for the user to register at `user/register`.
### 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()`.
#### Middleware
Registered for all routes is middleware that will load the currently logged in user entity and store it within the request context. The middleware is located at `middleware.LoadAuthenticatedUser()` and, if authenticated, the `User` entity is stored within the context using the key `context.AuthenticatedUserKey`.
If you wish to require either authentication or non-authentication for a given route, you can use either `middleware.RequireAuthentication()` or `middleware.RequireNoAuthentication()`.
## 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.
### Custom middleware
By default, a middleware stack is included in the router that makes sense for most web applications. Be sure to review what has been included and what else is available within _Echo_ and the other projects mentioned.
A `middleware` package is included which you can easily add to along with the custom middleware provided.
### Controller / Dependencies
The `Controller`, which is described in a section below, serves two purposes for routes:
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
While using the `Controller` is not required for your routes, it will certainly make development easier.
See the following section for the proposed pattern.
### 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`:
```go
type Home struct {
controller.Controller
}
func (c *Home) Get(ctx echo.Context) error {}
func (c *Home) Post(ctx echo.Context) error {}
```
Then create the route and add to the router:
```go
home := Home{Controller: controller.NewController(c)}
g.GET("/", home.Get).Name = "home"
g.POST("/", home.Post).Name = "home.post"
```
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`.
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.
#### 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`.
Here is an example how to easily make a request and evaluate the response:
```go
func TestAbout_Get(t *testing.T) {
doc := request(t).
setRoute("about").
get().
assertStatusCode(http.StatusOK).
toDoc()
}
```
#### Goquery
A helpful, included package to test HTML markup from HTTP responses is [goquery](https://github.com/PuerkitoBio/goquery). This allows you to use jQuery-style selectors to parse and extract HTML values, attributes, and so on.
In the example above, `toDoc()` will return a `*goquery.Document` created from the HTML response of the test HTTP server.
Here is a simple example of how to use it, along with [testify](https://github.com/stretchr/testify) for making assertions:
```go
h1 := doc.Find("h1.title")
assert.Len(t, h1.Nodes, 1)
assert.Equal(t, "About", h1.Text())
```
## Controller
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.
### Page
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.
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.
As you develop your application, the `Page` can be easily extended to include whatever data or functions you want to provide to your templates.
Initializing a new page is simple:
```go
func (c *Home) Get(ctx echo.Context) error {
page := controller.NewPage(ctx)
}
```
Using the `echo.Context`, the `Page` will be initialized with the following fields populated:
- `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
- `IsHome`: If the request was for the homepage
- `IsAuth`: If the user is authenticated
- `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)
### 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 requires that [sessions](#sessions) and the session middleware are in place since that is where the messages are stored.
#### Creating messages
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.
#### 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.
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.
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
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.
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
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:
```
<input id="email" name="email" type="email" class="input" value="{{.Form.Email}}">
```
#### 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:
```
<input id="email" name="email" type="email" class="input {{.Form.Submission.GetFieldStatusClass "Email"}}" value="{{.Form.Email}}">
```
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
<a href="/user/profile/1" class="is-active extra-class">Profile</a>
```
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.
## 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.
The built-in usage of the cache is currently only for optional [page caching](#cached-responses) but it can be used for practically anything.
## Static files
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.
### Cache control headers
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
<img src="{{File "picture.png"}"/>
```
Which would result in:
```html
<img src="/files/picture.png?v=9fhe73kaf3"/>
```
Where `9fhe73kaf3` is the randomly-generated cache-buster.
## Email
An email client was added as a _Service_ to the `Container` but it is just a skeleton without any actual email-sending functionality. The reason is because there are a lot of ways to send email and most prefer using a SaaS solution for that. That makes it difficult to provide a generic solution that will work for most applications.
Two starter methods were added to the `MailClient`, one to send an email via plain-text and one to send via a template by leveraging the [template renderer](#template-renderer). The standard library can be used if you wish to send email via SMTP and most SaaS providers have a Go package that can be used if you choose to go that direction.
## HTTPS
By default, the application will not use HTTPS but it can be enabled easily. Just alter the following configuration:
- `Config.HTTP.TLS.Enabled`: `true`
- `Config.HTTP.TLS.Certificate`: Full path to the certificate file
- `Config.HTTP.TLS.Key`: Full path to the key file
To use _Let's Encrypt_ follow [this guide](https://echo.labstack.com/cookbook/auto-tls/#server).
## Logging
Logging is provided by [Echo](https://echo.labstack.com/guide/customization/#logging) and is accessible within the _Echo_ instance, which is located in the `Web` field of the `Container`, or within any of the _context_ parameters, for example:
```go
func (c *Home) Get(ctx echo.Context) error {
ctx.Logger().Info("something happened")
if err := someOperation(); err != nil {
ctx.Logger().Errorf("the operation failed: %v", err)
}
}
```
The logger can be swapped out for another, as long as it implements Echo's logging [interface](https://github.com/labstack/echo/blob/master/log.go). There are projects that provide this bridge for popular logging packages such as [zerolog](https://github.com/rs/zerolog).
### Request ID
By default, Echo's [request ID middleware](https://echo.labstack.com/middleware/request-id/) is enabled on the router but it only adds a request ID to the log entry for the HTTP request itself. Log entries that are created during the course of that request do not contain the request ID. `LogRequestID()` is custom middleware included which adds that request ID to all logs created throughout the request.
## Roadmap
Future work includes but is not limited to:
- Email verification
- Flexible pager templates
- Expanded HTMX examples
- Admin section
## Credits
Thank you to all of the following amazing projects for making this possible.
- [go](https://go.dev/)
- [echo](https://github.com/labstack/echo)
- [ent](https://github.com/ent/ent)
- [sprig](https://github.com/Masterminds/sprig)
- [goquery](https://github.com/PuerkitoBio/goquery)
- [validator](https://github.com/go-playground/validator)
- [go-redis](https://github.com/go-redis/redis)
- [gocache](https://github.com/eko/gocache)
- [sessions](https://github.com/gorilla/sessions)
- [pgx](https://github.com/jackc/pgx)
- [envdecode](https://github.com/joeshaw/envdecode)
- [testify](https://github.com/stretchr/testify)
- [htmx](https://github.com/bigskysoftware/htmx)
- [alpinejs](https://github.com/alpinejs/alpine)
- [bulma](https://github.com/jgthms/bulma)
- [docker](https://www.docker.com/)
- [postgresql](https://www.postgresql.org/)
- [redis](https://redis.io/)
MIT — see LICENSE. Original Pagoda code Copyright (c) 2021 Mike Stefanello.

63
cmd/admin/main.go Normal file
View file

@ -0,0 +1,63 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"github.com/camzawacki/personal-site/pkg/log"
"github.com/camzawacki/personal-site/pkg/services"
)
// main creates a new admin user with the email passed in via the flag.
func main() {
// Start a new container.
c := services.NewContainer()
defer func() {
// Gracefully shutdown all services.
if err := c.Shutdown(); err != nil {
log.Default().Error("shutdown failed", "error", err)
}
}()
var email string
flag.StringVar(&email, "email", "", "email address for the admin user")
flag.Parse()
if len(email) == 0 {
invalid("email is required")
}
// Generate a password.
pw, err := c.Auth.RandomToken(10)
if err != nil {
invalid("failed to generate a random password")
}
// Create the admin user.
err = c.ORM.User.
Create().
SetEmail(email).
SetName("Admin").
SetAdmin(true).
SetVerified(true).
SetPassword(pw).
Exec(context.Background())
if err != nil {
invalid(err.Error())
}
fmt.Println("")
fmt.Println("-- ADMIN USER CREATED --")
fmt.Printf("Email: %s\n", email)
fmt.Printf("Password: %s\n", pw)
fmt.Println("----")
fmt.Println("")
}
func invalid(msg string) {
fmt.Printf("[ERROR] %s\n", msg)
os.Exit(1)
}

74
cmd/web/main.go Normal file
View file

@ -0,0 +1,74 @@
package main
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"github.com/camzawacki/personal-site/pkg/handlers"
"github.com/camzawacki/personal-site/pkg/log"
"github.com/camzawacki/personal-site/pkg/services"
"github.com/camzawacki/personal-site/pkg/tasks"
)
func main() {
// Start a new container.
c := services.NewContainer()
defer func() {
// Gracefully shutdown all services.
fatal("shutdown failed", c.Shutdown())
}()
// Build the router.
if err := handlers.BuildRouter(c); err != nil {
fatal("failed to build the router", err)
}
// Register all task queues.
tasks.Register(c)
// Start the task runner to execute queued tasks.
c.Tasks.Start(context.Background())
// Start the server.
go func() {
srv := http.Server{
Addr: fmt.Sprintf("%s:%d", c.Config.HTTP.Hostname, c.Config.HTTP.Port),
Handler: c.Web,
ReadTimeout: c.Config.HTTP.ReadTimeout,
WriteTimeout: c.Config.HTTP.WriteTimeout,
IdleTimeout: c.Config.HTTP.IdleTimeout,
}
if c.Config.HTTP.TLS.Enabled {
certs, err := tls.LoadX509KeyPair(c.Config.HTTP.TLS.Certificate, c.Config.HTTP.TLS.Key)
fatal("cannot load TLS certificate", err)
srv.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{certs},
}
}
if err := c.Web.StartServer(&srv); errors.Is(err, http.ErrServerClosed) {
fatal("shutting down the server", err)
}
}()
// Wait for interrupt signal to gracefully shut down the web server and task runner.
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
signal.Notify(quit, os.Kill)
<-quit
}
// fatal logs an error and terminates the application, if the error is not nil.
func fatal(msg string, err error) {
if err != nil {
log.Default().Error(msg, "error", err)
os.Exit(1)
}
}

View file

@ -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"`
Hostname string
Port uint16
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
ShutdownTimeout time.Duration
TLS struct {
Enabled bool `env:"HTTP_TLS_ENABLED,default=false"`
Certificate string `env:"HTTP_TLS_CERTIFICATE"`
Key string `env:"HTTP_TLS_KEY"`
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
}

54
config/config.yaml Normal file
View file

@ -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"

View file

@ -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()

View file

@ -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"
)

View file

@ -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)
}

View file

@ -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)
})
}

View file

@ -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)
}
}

View file

@ -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())
}

View file

@ -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
}

View file

@ -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))
}
}
}

View file

@ -1,15 +1,24 @@
version: "3"
services:
cache:
image: "redis:alpine"
ports:
- "6379:6379"
db:
image: postgres:alpine
ports:
- "5432:5432"
app:
build: .
container_name: personal-site
restart: unless-stopped
environment:
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=admin
- POSTGRES_DB=app
- PAGODA_APP_ENVIRONMENT=production
- PAGODA_APP_HOST=camzalewski.com
- PAGODA_HTTP_HOSTNAME=0.0.0.0
- PAGODA_HTTP_PORT=8000
- PAGODA_APP_ENCRYPTIONKEY=${ENCRYPTION_KEY}
volumes:
- sqlite_data:/app/dbs
- uploads:/app/uploads
networks:
- web
volumes:
sqlite_data:
uploads:
networks:
web:
external: true

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

@ -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)
}

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

@ -0,0 +1,319 @@
// Code generated by ent, DO NOT EDIT.
package admin
import (
"fmt"
"net/url"
"strconv"
"time"
"entgo.io/ent/dialect/sql"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/ent"
"github.com/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/ent/user"
)
const dateTimeFormat = "2006-01-02T15:04:05"
const dateTimeFormatNoSeconds = "2006-01-02T15:04"
type Handler struct {
client *ent.Client
Config HandlerConfig
}
func NewHandler(client *ent.Client, cfg HandlerConfig) *Handler {
return &Handler{
client: client,
Config: cfg,
}
}
func (h *Handler) Create(ctx echo.Context, entityType EntityType) error {
switch entityType.(type) {
case *PasswordToken:
return h.PasswordTokenCreate(ctx)
case *User:
return h.UserCreate(ctx)
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) Get(ctx echo.Context, entityType EntityType, id int) (url.Values, error) {
switch entityType.(type) {
case *PasswordToken:
return h.PasswordTokenGet(ctx, id)
case *User:
return h.UserGet(ctx, id)
default:
return nil, fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) Delete(ctx echo.Context, entityType EntityType, id int) error {
switch entityType.(type) {
case *PasswordToken:
return h.PasswordTokenDelete(ctx, id)
case *User:
return h.UserDelete(ctx, id)
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) Update(ctx echo.Context, entityType EntityType, id int) error {
switch entityType.(type) {
case *PasswordToken:
return h.PasswordTokenUpdate(ctx, id)
case *User:
return h.UserUpdate(ctx, id)
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) List(ctx echo.Context, entityType EntityType) (*EntityList, error) {
switch entityType.(type) {
case *PasswordToken:
return h.PasswordTokenList(ctx)
case *User:
return h.UserList(ctx)
default:
return nil, fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (h *Handler) PasswordTokenCreate(ctx echo.Context) error {
var payload PasswordToken
if err := h.bind(ctx, &payload); err != nil {
return err
}
op := h.client.PasswordToken.Create()
if payload.Token != nil {
op.SetToken(*payload.Token)
}
op.SetUserID(payload.UserID)
if payload.CreatedAt != nil {
op.SetCreatedAt(*payload.CreatedAt)
}
_, err := op.Save(ctx.Request().Context())
return err
}
func (h *Handler) PasswordTokenUpdate(ctx echo.Context, id int) error {
entity, err := h.client.PasswordToken.Get(ctx.Request().Context(), id)
if err != nil {
return err
}
var payload PasswordToken
if err = h.bind(ctx, &payload); err != nil {
return err
}
op := entity.Update()
if payload.Token != nil {
op.SetToken(*payload.Token)
}
op.SetUserID(payload.UserID)
if payload.CreatedAt == nil {
var empty time.Time
op.SetCreatedAt(empty)
} else {
op.SetCreatedAt(*payload.CreatedAt)
}
_, err = op.Save(ctx.Request().Context())
return err
}
func (h *Handler) PasswordTokenDelete(ctx echo.Context, id int) error {
return h.client.PasswordToken.DeleteOneID(id).
Exec(ctx.Request().Context())
}
func (h *Handler) PasswordTokenList(ctx echo.Context) (*EntityList, error) {
page, offset := h.getPageAndOffset(ctx)
res, err := h.client.PasswordToken.
Query().
Limit(h.Config.ItemsPerPage + 1).
Offset(offset).
Order(passwordtoken.ByID(sql.OrderDesc())).
All(ctx.Request().Context())
if err != nil {
return nil, err
}
list := &EntityList{
Columns: []string{
"User ID",
"Created at",
},
Entities: make([]EntityValues, 0, len(res)),
Page: page,
HasNextPage: len(res) > h.Config.ItemsPerPage,
}
for i := 0; i <= len(res)-1; i++ {
list.Entities = append(list.Entities, EntityValues{
ID: res[i].ID,
Values: []string{
fmt.Sprint(res[i].UserID),
res[i].CreatedAt.Format(h.Config.TimeFormat),
},
})
}
return list, err
}
func (h *Handler) PasswordTokenGet(ctx echo.Context, id int) (url.Values, error) {
entity, err := h.client.PasswordToken.Get(ctx.Request().Context(), id)
if err != nil {
return nil, err
}
v := url.Values{}
v.Set("user_id", fmt.Sprint(entity.UserID))
v.Set("created_at", entity.CreatedAt.Format(dateTimeFormat))
return v, err
}
func (h *Handler) UserCreate(ctx echo.Context) error {
var payload User
if err := h.bind(ctx, &payload); err != nil {
return err
}
op := h.client.User.Create()
op.SetName(payload.Name)
op.SetEmail(payload.Email)
if payload.Password != nil {
op.SetPassword(*payload.Password)
}
op.SetVerified(payload.Verified)
op.SetAdmin(payload.Admin)
if payload.CreatedAt != nil {
op.SetCreatedAt(*payload.CreatedAt)
}
_, err := op.Save(ctx.Request().Context())
return err
}
func (h *Handler) UserUpdate(ctx echo.Context, id int) error {
entity, err := h.client.User.Get(ctx.Request().Context(), id)
if err != nil {
return err
}
var payload User
if err = h.bind(ctx, &payload); err != nil {
return err
}
op := entity.Update()
op.SetName(payload.Name)
op.SetEmail(payload.Email)
if payload.Password != nil {
op.SetPassword(*payload.Password)
}
op.SetVerified(payload.Verified)
op.SetAdmin(payload.Admin)
_, err = op.Save(ctx.Request().Context())
return err
}
func (h *Handler) UserDelete(ctx echo.Context, id int) error {
return h.client.User.DeleteOneID(id).
Exec(ctx.Request().Context())
}
func (h *Handler) UserList(ctx echo.Context) (*EntityList, error) {
page, offset := h.getPageAndOffset(ctx)
res, err := h.client.User.
Query().
Limit(h.Config.ItemsPerPage + 1).
Offset(offset).
Order(user.ByID(sql.OrderDesc())).
All(ctx.Request().Context())
if err != nil {
return nil, err
}
list := &EntityList{
Columns: []string{
"Name",
"Email",
"Verified",
"Admin",
"Created at",
},
Entities: make([]EntityValues, 0, len(res)),
Page: page,
HasNextPage: len(res) > h.Config.ItemsPerPage,
}
for i := 0; i <= len(res)-1; i++ {
list.Entities = append(list.Entities, EntityValues{
ID: res[i].ID,
Values: []string{
res[i].Name,
res[i].Email,
fmt.Sprint(res[i].Verified),
fmt.Sprint(res[i].Admin),
res[i].CreatedAt.Format(h.Config.TimeFormat),
},
})
}
return list, err
}
func (h *Handler) UserGet(ctx echo.Context, id int) (url.Values, error) {
entity, err := h.client.User.Get(ctx.Request().Context(), id)
if err != nil {
return nil, err
}
v := url.Values{}
v.Set("name", entity.Name)
v.Set("email", entity.Email)
v.Set("verified", fmt.Sprint(entity.Verified))
v.Set("admin", fmt.Sprint(entity.Admin))
return v, err
}
func (h *Handler) getPageAndOffset(ctx echo.Context) (int, int) {
if page, err := strconv.Atoi(ctx.QueryParam(h.Config.PageQueryKey)); err == nil {
if page > 1 {
return page, (page - 1) * h.Config.ItemsPerPage
}
}
return 1, 0
}
func (h *Handler) bind(ctx echo.Context, entity any) error {
// Echo requires some pre-processing of form values to avoid problems.
for k, v := range ctx.Request().Form {
// Remove empty field values so Echo's bind does not fail when trying to parse things like
// times, etc.
if len(v) == 1 && len(v[0]) == 0 {
delete(ctx.Request().Form, k)
continue
}
// Echo expects datetime values to be in a certain format but that does not align with the datetime-local
// HTML form element format, so we will attempt to convert it here.
for _, format := range []string{dateTimeFormatNoSeconds, dateTimeFormat} {
if t, err := time.Parse(format, v[0]); err == nil {
ctx.Request().Form[k][0] = t.Format(time.RFC3339)
break
}
}
}
return ctx.Bind(entity)
}

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

@ -0,0 +1,101 @@
// Code generated by ent, DO NOT EDIT.
package admin
import (
"entgo.io/ent/schema/field"
)
type Enum struct {
Label, Value string
}
type FieldSchema struct {
Name string
Type field.Type
Optional bool
Immutable bool
Sensitive bool
Enums []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,
},
}

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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 }}

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

@ -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
}

View file

@ -1,20 +1,22 @@
// Code generated by entc, DO NOT EDIT.
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"errors"
"fmt"
"log"
"reflect"
"goweb/ent/migrate"
"goweb/ent/passwordtoken"
"goweb/ent/user"
"github.com/camzawacki/personal-site/ent/migrate"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/ent/user"
)
// Client is the client that holds all ent builders.
@ -30,9 +32,7 @@ type Client struct {
// NewClient creates a new client configured with the given options.
func NewClient(opts ...Option) *Client {
cfg := config{log: log.Println, hooks: &hooks{}}
cfg.options(opts...)
client := &Client{config: cfg}
client := &Client{config: newConfig(opts...)}
client.init()
return client
}
@ -43,6 +43,62 @@ func (c *Client) init() {
c.User = NewUserClient(c.config)
}
type (
// config is the configuration for the client and its builder.
config struct {
// driver used for executing database requests.
driver dialect.Driver
// debug enable a debug logging.
debug bool
// log used for logging on debug mode.
log func(...any)
// hooks to execute on mutations.
hooks *hooks
// interceptors to execute on queries.
inters *inters
}
// Option function to configure the client.
Option func(*config)
)
// newConfig creates a new config for the client.
func newConfig(opts ...Option) config {
cfg := config{log: log.Println, hooks: &hooks{}, inters: &inters{}}
cfg.options(opts...)
return cfg
}
// options applies the options on the config object.
func (c *config) options(opts ...Option) {
for _, opt := range opts {
opt(c)
}
if c.debug {
c.driver = dialect.Debug(c.driver, c.log)
}
}
// Debug enables debug logging on the ent.Driver.
func Debug() Option {
return func(c *config) {
c.debug = true
}
}
// Log sets the logging function for debug mode.
func Log(fn func(...any)) Option {
return func(c *config) {
c.log = fn
}
}
// Driver configures the client driver.
func Driver(driver dialect.Driver) Option {
return func(c *config) {
c.driver = driver
}
}
// Open opens a database/sql.DB specified by the driver name and
// the data source name, and returns a new client attached to it.
// Optional parameters can be added for configuring the client.
@ -59,11 +115,14 @@ func Open(driverName, dataSourceName string, options ...Option) (*Client, error)
}
}
// ErrTxStarted is returned when trying to start a new transaction from a transactional client.
var ErrTxStarted = errors.New("ent: cannot start a transaction within a transaction")
// Tx returns a new transactional client. The provided context
// is used until the transaction is committed or rolled back.
func (c *Client) Tx(ctx context.Context) (*Tx, error) {
if _, ok := c.driver.(*txDriver); ok {
return nil, fmt.Errorf("ent: cannot start a transaction within a transaction")
return nil, ErrTxStarted
}
tx, err := newTx(ctx, c.driver)
if err != nil {
@ -82,7 +141,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
// BeginTx returns a transactional client with specified options.
func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) {
if _, ok := c.driver.(*txDriver); ok {
return nil, fmt.Errorf("ent: cannot start a transaction within a transaction")
return nil, errors.New("ent: cannot start a transaction within a transaction")
}
tx, err := c.driver.(interface {
BeginTx(context.Context, *sql.TxOptions) (dialect.Tx, error)
@ -93,6 +152,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
cfg := c.config
cfg.driver = &txDriver{tx: tx, drv: c.driver}
return &Tx{
ctx: ctx,
config: cfg,
PasswordToken: NewPasswordTokenClient(cfg),
User: NewUserClient(cfg),
@ -105,7 +165,6 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
// PasswordToken.
// Query().
// Count(ctx)
//
func (c *Client) Debug() *Client {
if c.debug {
return c
@ -129,6 +188,25 @@ func (c *Client) Use(hooks ...Hook) {
c.User.Use(hooks...)
}
// Intercept adds the query interceptors to all the entity clients.
// In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`.
func (c *Client) Intercept(interceptors ...Interceptor) {
c.PasswordToken.Intercept(interceptors...)
c.User.Intercept(interceptors...)
}
// Mutate implements the ent.Mutator interface.
func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
switch m := m.(type) {
case *PasswordTokenMutation:
return c.PasswordToken.mutate(ctx, m)
case *UserMutation:
return c.User.mutate(ctx, m)
default:
return nil, fmt.Errorf("ent: unknown mutation type %T", m)
}
}
// PasswordTokenClient is a client for the PasswordToken schema.
type PasswordTokenClient struct {
config
@ -145,7 +223,13 @@ func (c *PasswordTokenClient) Use(hooks ...Hook) {
c.hooks.PasswordToken = append(c.hooks.PasswordToken, hooks...)
}
// Create returns a create builder for PasswordToken.
// Intercept adds a list of query interceptors to the interceptors stack.
// A call to `Intercept(f, g, h)` equals to `passwordtoken.Intercept(f(g(h())))`.
func (c *PasswordTokenClient) Intercept(interceptors ...Interceptor) {
c.inters.PasswordToken = append(c.inters.PasswordToken, interceptors...)
}
// Create returns a builder for creating a PasswordToken entity.
func (c *PasswordTokenClient) Create() *PasswordTokenCreate {
mutation := newPasswordTokenMutation(c.config, OpCreate)
return &PasswordTokenCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
@ -156,6 +240,21 @@ func (c *PasswordTokenClient) CreateBulk(builders ...*PasswordTokenCreate) *Pass
return &PasswordTokenCreateBulk{config: c.config, builders: builders}
}
// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
// a builder and applies setFunc on it.
func (c *PasswordTokenClient) MapCreateBulk(slice any, setFunc func(*PasswordTokenCreate, int)) *PasswordTokenCreateBulk {
rv := reflect.ValueOf(slice)
if rv.Kind() != reflect.Slice {
return &PasswordTokenCreateBulk{err: fmt.Errorf("calling to PasswordTokenClient.MapCreateBulk with wrong type %T, need slice", slice)}
}
builders := make([]*PasswordTokenCreate, rv.Len())
for i := 0; i < rv.Len(); i++ {
builders[i] = c.Create()
setFunc(builders[i], i)
}
return &PasswordTokenCreateBulk{config: c.config, builders: builders}
}
// Update returns an update builder for PasswordToken.
func (c *PasswordTokenClient) Update() *PasswordTokenUpdate {
mutation := newPasswordTokenMutation(c.config, OpUpdate)
@ -163,8 +262,8 @@ func (c *PasswordTokenClient) Update() *PasswordTokenUpdate {
}
// UpdateOne returns an update builder for the given entity.
func (c *PasswordTokenClient) UpdateOne(pt *PasswordToken) *PasswordTokenUpdateOne {
mutation := newPasswordTokenMutation(c.config, OpUpdateOne, withPasswordToken(pt))
func (c *PasswordTokenClient) UpdateOne(_m *PasswordToken) *PasswordTokenUpdateOne {
mutation := newPasswordTokenMutation(c.config, OpUpdateOne, withPasswordToken(_m))
return &PasswordTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
@ -180,12 +279,12 @@ func (c *PasswordTokenClient) Delete() *PasswordTokenDelete {
return &PasswordTokenDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// DeleteOne returns a delete builder for the given entity.
func (c *PasswordTokenClient) DeleteOne(pt *PasswordToken) *PasswordTokenDeleteOne {
return c.DeleteOneID(pt.ID)
// DeleteOne returns a builder for deleting the given entity.
func (c *PasswordTokenClient) DeleteOne(_m *PasswordToken) *PasswordTokenDeleteOne {
return c.DeleteOneID(_m.ID)
}
// DeleteOneID returns a delete builder for the given id.
// DeleteOneID returns a builder for deleting the given entity by its id.
func (c *PasswordTokenClient) DeleteOneID(id int) *PasswordTokenDeleteOne {
builder := c.Delete().Where(passwordtoken.ID(id))
builder.mutation.id = &id
@ -197,6 +296,8 @@ func (c *PasswordTokenClient) DeleteOneID(id int) *PasswordTokenDeleteOne {
func (c *PasswordTokenClient) Query() *PasswordTokenQuery {
return &PasswordTokenQuery{
config: c.config,
ctx: &QueryContext{Type: TypePasswordToken},
inters: c.Interceptors(),
}
}
@ -215,16 +316,16 @@ func (c *PasswordTokenClient) GetX(ctx context.Context, id int) *PasswordToken {
}
// QueryUser queries the user edge of a PasswordToken.
func (c *PasswordTokenClient) QueryUser(pt *PasswordToken) *UserQuery {
query := &UserQuery{config: c.config}
query.path = func(ctx context.Context) (fromV *sql.Selector, _ error) {
id := pt.ID
func (c *PasswordTokenClient) QueryUser(_m *PasswordToken) *UserQuery {
query := (&UserClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(passwordtoken.Table, passwordtoken.FieldID, id),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2O, false, passwordtoken.UserTable, passwordtoken.UserColumn),
)
fromV = sqlgraph.Neighbors(pt.driver.Dialect(), step)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
@ -232,7 +333,28 @@ func (c *PasswordTokenClient) QueryUser(pt *PasswordToken) *UserQuery {
// Hooks returns the client hooks.
func (c *PasswordTokenClient) Hooks() []Hook {
return c.hooks.PasswordToken
hooks := c.hooks.PasswordToken
return append(hooks[:len(hooks):len(hooks)], passwordtoken.Hooks[:]...)
}
// Interceptors returns the client interceptors.
func (c *PasswordTokenClient) Interceptors() []Interceptor {
return c.inters.PasswordToken
}
func (c *PasswordTokenClient) mutate(ctx context.Context, m *PasswordTokenMutation) (Value, error) {
switch m.Op() {
case OpCreate:
return (&PasswordTokenCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdate:
return (&PasswordTokenUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdateOne:
return (&PasswordTokenUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpDelete, OpDeleteOne:
return (&PasswordTokenDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
default:
return nil, fmt.Errorf("ent: unknown PasswordToken mutation op: %q", m.Op())
}
}
// UserClient is a client for the User schema.
@ -251,7 +373,13 @@ func (c *UserClient) Use(hooks ...Hook) {
c.hooks.User = append(c.hooks.User, hooks...)
}
// Create returns a create builder for User.
// Intercept adds a list of query interceptors to the interceptors stack.
// A call to `Intercept(f, g, h)` equals to `user.Intercept(f(g(h())))`.
func (c *UserClient) Intercept(interceptors ...Interceptor) {
c.inters.User = append(c.inters.User, interceptors...)
}
// Create returns a builder for creating a User entity.
func (c *UserClient) Create() *UserCreate {
mutation := newUserMutation(c.config, OpCreate)
return &UserCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
@ -262,6 +390,21 @@ func (c *UserClient) CreateBulk(builders ...*UserCreate) *UserCreateBulk {
return &UserCreateBulk{config: c.config, builders: builders}
}
// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
// a builder and applies setFunc on it.
func (c *UserClient) MapCreateBulk(slice any, setFunc func(*UserCreate, int)) *UserCreateBulk {
rv := reflect.ValueOf(slice)
if rv.Kind() != reflect.Slice {
return &UserCreateBulk{err: fmt.Errorf("calling to UserClient.MapCreateBulk with wrong type %T, need slice", slice)}
}
builders := make([]*UserCreate, rv.Len())
for i := 0; i < rv.Len(); i++ {
builders[i] = c.Create()
setFunc(builders[i], i)
}
return &UserCreateBulk{config: c.config, builders: builders}
}
// Update returns an update builder for User.
func (c *UserClient) Update() *UserUpdate {
mutation := newUserMutation(c.config, OpUpdate)
@ -269,8 +412,8 @@ func (c *UserClient) Update() *UserUpdate {
}
// UpdateOne returns an update builder for the given entity.
func (c *UserClient) UpdateOne(u *User) *UserUpdateOne {
mutation := newUserMutation(c.config, OpUpdateOne, withUser(u))
func (c *UserClient) UpdateOne(_m *User) *UserUpdateOne {
mutation := newUserMutation(c.config, OpUpdateOne, withUser(_m))
return &UserUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
@ -286,12 +429,12 @@ func (c *UserClient) Delete() *UserDelete {
return &UserDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// DeleteOne returns a delete builder for the given entity.
func (c *UserClient) DeleteOne(u *User) *UserDeleteOne {
return c.DeleteOneID(u.ID)
// DeleteOne returns a builder for deleting the given entity.
func (c *UserClient) DeleteOne(_m *User) *UserDeleteOne {
return c.DeleteOneID(_m.ID)
}
// DeleteOneID returns a delete builder for the given id.
// DeleteOneID returns a builder for deleting the given entity by its id.
func (c *UserClient) DeleteOneID(id int) *UserDeleteOne {
builder := c.Delete().Where(user.ID(id))
builder.mutation.id = &id
@ -303,6 +446,8 @@ func (c *UserClient) DeleteOneID(id int) *UserDeleteOne {
func (c *UserClient) Query() *UserQuery {
return &UserQuery{
config: c.config,
ctx: &QueryContext{Type: TypeUser},
inters: c.Interceptors(),
}
}
@ -321,16 +466,16 @@ func (c *UserClient) GetX(ctx context.Context, id int) *User {
}
// QueryOwner queries the owner edge of a User.
func (c *UserClient) QueryOwner(u *User) *PasswordTokenQuery {
query := &PasswordTokenQuery{config: c.config}
query.path = func(ctx context.Context) (fromV *sql.Selector, _ error) {
id := u.ID
func (c *UserClient) QueryOwner(_m *User) *PasswordTokenQuery {
query := (&PasswordTokenClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID, id),
sqlgraph.To(passwordtoken.Table, passwordtoken.FieldID),
sqlgraph.Edge(sqlgraph.O2M, true, user.OwnerTable, user.OwnerColumn),
)
fromV = sqlgraph.Neighbors(u.driver.Dialect(), step)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
@ -341,3 +486,33 @@ func (c *UserClient) Hooks() []Hook {
hooks := c.hooks.User
return append(hooks[:len(hooks):len(hooks)], user.Hooks[:]...)
}
// Interceptors returns the client interceptors.
func (c *UserClient) Interceptors() []Interceptor {
return c.inters.User
}
func (c *UserClient) mutate(ctx context.Context, m *UserMutation) (Value, error) {
switch m.Op() {
case OpCreate:
return (&UserCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdate:
return (&UserUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdateOne:
return (&UserUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpDelete, OpDeleteOne:
return (&UserDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
default:
return nil, fmt.Errorf("ent: unknown User mutation op: %q", m.Op())
}
}
// hooks and interceptors per client, for fast access.
type (
hooks struct {
PasswordToken, User []ent.Hook
}
inters struct {
PasswordToken, User []ent.Interceptor
}
)

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -1,15 +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/user"
"reflect"
"sync"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/ent/user"
)
// ent aliases to avoid import conflicts in user's code.
@ -18,41 +22,70 @@ type (
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{
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,
}
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
}
})
})
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)

22
ent/entc.go Normal file
View file

@ -0,0 +1,22 @@
//go:build ignore
// +build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/camzawacki/personal-site/ent/admin"
)
func main() {
err := entc.Generate("./schema",
&gen.Config{},
entc.Extensions(&admin.Extension{}),
)
if err != nil {
log.Fatal("running ent codegen:", err)
}
}

View file

@ -1,14 +1,16 @@
// Code generated by entc, DO NOT EDIT.
// Code generated by ent, DO NOT EDIT.
package enttest
import (
"context"
"goweb/ent"
"github.com/camzawacki/personal-site/ent"
// required by schema hooks.
_ "goweb/ent/runtime"
_ "github.com/camzawacki/personal-site/ent/runtime"
"entgo.io/ent/dialect/sql/schema"
"github.com/camzawacki/personal-site/ent/migrate"
)
type (
@ -16,7 +18,7 @@ type (
// testing.T and testing.B and used by enttest.
TestingT interface {
FailNow()
Error(...interface{})
Error(...any)
}
// Option configures client creation.
@ -58,10 +60,7 @@ func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Cl
t.Error(err)
t.FailNow()
}
if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
t.Error(err)
t.FailNow()
}
migrateSchema(t, c, o)
return c
}
@ -69,9 +68,17 @@ func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Cl
func NewClient(t TestingT, opts ...Option) *ent.Client {
o := newOptions(opts)
c := ent.NewClient(o.opts...)
if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
migrateSchema(t, c, o)
return c
}
func migrateSchema(t TestingT, c *ent.Client, o *options) {
tables, err := schema.CopyTables(migrate.Tables)
if err != nil {
t.Error(err)
t.FailNow()
}
if err := migrate.Create(context.Background(), c.Schema, tables, o.migrateOpts...); err != nil {
t.Error(err)
t.FailNow()
}
return c
}

View file

@ -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

View file

@ -1,11 +1,12 @@
// Code generated by entc, DO NOT EDIT.
// Code generated by ent, DO NOT EDIT.
package hook
import (
"context"
"fmt"
"goweb/ent"
"github.com/camzawacki/personal-site/ent"
)
// The PasswordTokenFunc type is an adapter to allow the use of ordinary
@ -14,11 +15,10 @@ type PasswordTokenFunc func(context.Context, *ent.PasswordTokenMutation) (ent.Va
// Mutate calls f(ctx, m).
func (f PasswordTokenFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
mv, ok := m.(*ent.PasswordTokenMutation)
if !ok {
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.PasswordTokenMutation", m)
}
if mv, ok := m.(*ent.PasswordTokenMutation); ok {
return f(ctx, mv)
}
return 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 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)

View file

@ -1,4 +1,4 @@
// Code generated by entc, DO NOT EDIT.
// Code generated by ent, DO NOT EDIT.
package migrate
@ -28,9 +28,6 @@ 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
)
@ -38,7 +35,6 @@ var (
// Schema is the API for creating, migrating and dropping a schema.
type Schema struct {
drv dialect.Driver
universalID bool
}
// NewSchema creates a new schema client.
@ -46,11 +42,16 @@ 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.
@ -58,15 +59,6 @@ func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error
// 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...)
}

View file

@ -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.

View file

@ -1,17 +1,19 @@
// Code generated by entc, DO NOT EDIT.
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
"errors"
"fmt"
"goweb/ent/passwordtoken"
"goweb/ent/predicate"
"goweb/ent/user"
"sync"
"time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"github.com/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/ent/predicate"
"github.com/camzawacki/personal-site/ent/user"
)
const (
@ -33,7 +35,7 @@ type PasswordTokenMutation struct {
op Op
typ string
id *int
hash *string
token *string
created_at *time.Time
clearedFields map[string]struct{}
user *int
@ -73,7 +75,7 @@ func withPasswordTokenID(id int) passwordtokenOption {
m.oldValue = func(ctx context.Context) (*PasswordToken, error) {
once.Do(func() {
if m.done {
err = fmt.Errorf("querying old values post mutation is not allowed")
err = errors.New("querying old values post mutation is not allowed")
} else {
value, err = m.Client().PasswordToken.Get(ctx, id)
}
@ -106,7 +108,7 @@ func (m PasswordTokenMutation) Client() *Client {
// it returns an error otherwise.
func (m PasswordTokenMutation) Tx() (*Tx, error) {
if _, ok := m.driver.(*txDriver); !ok {
return nil, fmt.Errorf("ent: mutation is not running in a transaction")
return nil, errors.New("ent: mutation is not running in a transaction")
}
tx := &Tx{config: m.config}
tx.init()
@ -122,40 +124,95 @@ func (m *PasswordTokenMutation) ID() (id int, exists bool) {
return *m.id, true
}
// SetHash sets the "hash" field.
func (m *PasswordTokenMutation) SetHash(s string) {
m.hash = &s
// IDs queries the database and returns the entity ids that match the mutation's predicate.
// That means, if the mutation is applied within a transaction with an isolation level such
// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated
// or updated by the mutation.
func (m *PasswordTokenMutation) IDs(ctx context.Context) ([]int, error) {
switch {
case m.op.Is(OpUpdateOne | OpDeleteOne):
id, exists := m.ID()
if exists {
return []int{id}, nil
}
fallthrough
case m.op.Is(OpUpdate | OpDelete):
return m.Client().PasswordToken.Query().Where(m.predicates...).IDs(ctx)
default:
return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op)
}
}
// Hash returns the value of the "hash" field in the mutation.
func (m *PasswordTokenMutation) Hash() (r string, exists bool) {
v := m.hash
// SetToken sets the "token" field.
func (m *PasswordTokenMutation) SetToken(s string) {
m.token = &s
}
// Token returns the value of the "token" field in the mutation.
func (m *PasswordTokenMutation) Token() (r string, exists bool) {
v := m.token
if v == nil {
return
}
return *v, true
}
// OldHash returns the old "hash" field's value of the PasswordToken entity.
// OldToken returns the old "token" field's value of the PasswordToken entity.
// If the PasswordToken object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *PasswordTokenMutation) OldHash(ctx context.Context) (v string, err error) {
func (m *PasswordTokenMutation) OldToken(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, fmt.Errorf("OldHash is only allowed on UpdateOne operations")
return v, errors.New("OldToken is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, fmt.Errorf("OldHash requires an ID field in the mutation")
return v, errors.New("OldToken requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldHash: %w", err)
return v, fmt.Errorf("querying old value for OldToken: %w", err)
}
return oldValue.Hash, nil
return oldValue.Token, nil
}
// ResetHash resets all changes to the "hash" field.
func (m *PasswordTokenMutation) ResetHash() {
m.hash = nil
// ResetToken resets all changes to the "token" field.
func (m *PasswordTokenMutation) ResetToken() {
m.token = nil
}
// SetUserID sets the "user_id" field.
func (m *PasswordTokenMutation) SetUserID(i int) {
m.user = &i
}
// UserID returns the value of the "user_id" field in the mutation.
func (m *PasswordTokenMutation) UserID() (r int, exists bool) {
v := m.user
if v == nil {
return
}
return *v, true
}
// OldUserID returns the old "user_id" field's value of the PasswordToken entity.
// If the PasswordToken object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *PasswordTokenMutation) OldUserID(ctx context.Context) (v int, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldUserID is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldUserID requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldUserID: %w", err)
}
return oldValue.UserID, nil
}
// ResetUserID resets all changes to the "user_id" field.
func (m *PasswordTokenMutation) ResetUserID() {
m.user = nil
}
// SetCreatedAt sets the "created_at" field.
@ -177,10 +234,10 @@ func (m *PasswordTokenMutation) CreatedAt() (r time.Time, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *PasswordTokenMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) {
if !m.op.Is(OpUpdateOne) {
return v, fmt.Errorf("OldCreatedAt is only allowed on UpdateOne operations")
return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, fmt.Errorf("OldCreatedAt requires an ID field in the mutation")
return v, errors.New("OldCreatedAt requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@ -194,14 +251,10 @@ func (m *PasswordTokenMutation) ResetCreatedAt() {
m.created_at = nil
}
// SetUserID sets the "user" edge to the User entity by id.
func (m *PasswordTokenMutation) SetUserID(id int) {
m.user = &id
}
// ClearUser clears the "user" edge to the User entity.
func (m *PasswordTokenMutation) ClearUser() {
m.cleareduser = true
m.clearedFields[passwordtoken.FieldUserID] = struct{}{}
}
// UserCleared reports if the "user" edge to the User entity was cleared.
@ -209,14 +262,6 @@ func (m *PasswordTokenMutation) UserCleared() bool {
return m.cleareduser
}
// UserID returns the "user" edge ID in the mutation.
func (m *PasswordTokenMutation) UserID() (id int, exists bool) {
if m.user != nil {
return *m.user, true
}
return
}
// UserIDs returns the "user" edge IDs in the mutation.
// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use
// UserID instead. It exists only for internal usage by the builders.
@ -238,11 +283,26 @@ func (m *PasswordTokenMutation) Where(ps ...predicate.PasswordToken) {
m.predicates = append(m.predicates, ps...)
}
// WhereP appends storage-level predicates to the PasswordTokenMutation builder. Using this method,
// users can use type-assertion to append predicates that do not depend on any generated package.
func (m *PasswordTokenMutation) WhereP(ps ...func(*sql.Selector)) {
p := make([]predicate.PasswordToken, len(ps))
for i := range ps {
p[i] = ps[i]
}
m.Where(p...)
}
// Op returns the operation name.
func (m *PasswordTokenMutation) Op() Op {
return m.op
}
// SetOp allows setting the mutation operation.
func (m *PasswordTokenMutation) SetOp(op Op) {
m.op = op
}
// Type returns the node type of this mutation (PasswordToken).
func (m *PasswordTokenMutation) Type() string {
return m.typ
@ -252,9 +312,12 @@ func (m *PasswordTokenMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *PasswordTokenMutation) Fields() []string {
fields := make([]string, 0, 2)
if m.hash != nil {
fields = append(fields, passwordtoken.FieldHash)
fields := make([]string, 0, 3)
if m.token != nil {
fields = append(fields, passwordtoken.FieldToken)
}
if m.user != nil {
fields = append(fields, passwordtoken.FieldUserID)
}
if m.created_at != nil {
fields = append(fields, passwordtoken.FieldCreatedAt)
@ -267,8 +330,10 @@ func (m *PasswordTokenMutation) Fields() []string {
// schema.
func (m *PasswordTokenMutation) Field(name string) (ent.Value, bool) {
switch name {
case passwordtoken.FieldHash:
return m.Hash()
case passwordtoken.FieldToken:
return m.Token()
case passwordtoken.FieldUserID:
return m.UserID()
case passwordtoken.FieldCreatedAt:
return m.CreatedAt()
}
@ -280,8 +345,10 @@ func (m *PasswordTokenMutation) Field(name string) (ent.Value, bool) {
// database failed.
func (m *PasswordTokenMutation) OldField(ctx context.Context, name string) (ent.Value, error) {
switch name {
case passwordtoken.FieldHash:
return m.OldHash(ctx)
case passwordtoken.FieldToken:
return m.OldToken(ctx)
case passwordtoken.FieldUserID:
return m.OldUserID(ctx)
case passwordtoken.FieldCreatedAt:
return m.OldCreatedAt(ctx)
}
@ -293,12 +360,19 @@ func (m *PasswordTokenMutation) OldField(ctx context.Context, name string) (ent.
// type.
func (m *PasswordTokenMutation) SetField(name string, value ent.Value) error {
switch name {
case passwordtoken.FieldHash:
case passwordtoken.FieldToken:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetHash(v)
m.SetToken(v)
return nil
case passwordtoken.FieldUserID:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetUserID(v)
return nil
case passwordtoken.FieldCreatedAt:
v, ok := value.(time.Time)
@ -314,13 +388,16 @@ func (m *PasswordTokenMutation) SetField(name string, value ent.Value) error {
// AddedFields returns all numeric fields that were incremented/decremented during
// this mutation.
func (m *PasswordTokenMutation) AddedFields() []string {
return nil
var fields []string
return fields
}
// AddedField returns the numeric value that was incremented/decremented on a field
// with the given name. The second boolean return value indicates that this field
// was not set, or was not defined in the schema.
func (m *PasswordTokenMutation) AddedField(name string) (ent.Value, bool) {
switch name {
}
return nil, false
}
@ -356,8 +433,11 @@ func (m *PasswordTokenMutation) ClearField(name string) error {
// It returns an error if the field is not defined in the schema.
func (m *PasswordTokenMutation) ResetField(name string) error {
switch name {
case passwordtoken.FieldHash:
m.ResetHash()
case passwordtoken.FieldToken:
m.ResetToken()
return nil
case passwordtoken.FieldUserID:
m.ResetUserID()
return nil
case passwordtoken.FieldCreatedAt:
m.ResetCreatedAt()
@ -396,8 +476,6 @@ func (m *PasswordTokenMutation) RemovedEdges() []string {
// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with
// the given name in this mutation.
func (m *PasswordTokenMutation) RemovedIDs(name string) []ent.Value {
switch name {
}
return nil
}
@ -451,6 +529,8 @@ type UserMutation struct {
name *string
email *string
password *string
verified *bool
admin *bool
created_at *time.Time
clearedFields map[string]struct{}
owner map[int]struct{}
@ -491,7 +571,7 @@ func withUserID(id int) userOption {
m.oldValue = func(ctx context.Context) (*User, error) {
once.Do(func() {
if m.done {
err = fmt.Errorf("querying old values post mutation is not allowed")
err = errors.New("querying old values post mutation is not allowed")
} else {
value, err = m.Client().User.Get(ctx, id)
}
@ -524,7 +604,7 @@ func (m UserMutation) Client() *Client {
// it returns an error otherwise.
func (m UserMutation) Tx() (*Tx, error) {
if _, ok := m.driver.(*txDriver); !ok {
return nil, fmt.Errorf("ent: mutation is not running in a transaction")
return nil, errors.New("ent: mutation is not running in a transaction")
}
tx := &Tx{config: m.config}
tx.init()
@ -540,6 +620,25 @@ func (m *UserMutation) ID() (id int, exists bool) {
return *m.id, true
}
// IDs queries the database and returns the entity ids that match the mutation's predicate.
// That means, if the mutation is applied within a transaction with an isolation level such
// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated
// or updated by the mutation.
func (m *UserMutation) IDs(ctx context.Context) ([]int, error) {
switch {
case m.op.Is(OpUpdateOne | OpDeleteOne):
id, exists := m.ID()
if exists {
return []int{id}, nil
}
fallthrough
case m.op.Is(OpUpdate | OpDelete):
return m.Client().User.Query().Where(m.predicates...).IDs(ctx)
default:
return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op)
}
}
// SetName sets the "name" field.
func (m *UserMutation) SetName(s string) {
m.name = &s
@ -559,10 +658,10 @@ func (m *UserMutation) Name() (r string, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldName(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, fmt.Errorf("OldName is only allowed on UpdateOne operations")
return v, errors.New("OldName is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, fmt.Errorf("OldName requires an ID field in the mutation")
return v, errors.New("OldName requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@ -595,10 +694,10 @@ func (m *UserMutation) Email() (r string, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldEmail(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, fmt.Errorf("OldEmail is only allowed on UpdateOne operations")
return v, errors.New("OldEmail is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, fmt.Errorf("OldEmail requires an ID field in the mutation")
return v, errors.New("OldEmail requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@ -631,10 +730,10 @@ func (m *UserMutation) Password() (r string, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldPassword(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, fmt.Errorf("OldPassword is only allowed on UpdateOne operations")
return v, errors.New("OldPassword is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, fmt.Errorf("OldPassword requires an ID field in the mutation")
return v, errors.New("OldPassword requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@ -648,6 +747,78 @@ func (m *UserMutation) ResetPassword() {
m.password = nil
}
// SetVerified sets the "verified" field.
func (m *UserMutation) SetVerified(b bool) {
m.verified = &b
}
// Verified returns the value of the "verified" field in the mutation.
func (m *UserMutation) Verified() (r bool, exists bool) {
v := m.verified
if v == nil {
return
}
return *v, true
}
// OldVerified returns the old "verified" field's value of the User entity.
// If the User object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldVerified(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldVerified is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldVerified requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldVerified: %w", err)
}
return oldValue.Verified, nil
}
// ResetVerified resets all changes to the "verified" field.
func (m *UserMutation) ResetVerified() {
m.verified = nil
}
// SetAdmin sets the "admin" field.
func (m *UserMutation) SetAdmin(b bool) {
m.admin = &b
}
// Admin returns the value of the "admin" field in the mutation.
func (m *UserMutation) Admin() (r bool, exists bool) {
v := m.admin
if v == nil {
return
}
return *v, true
}
// OldAdmin returns the old "admin" field's value of the User entity.
// If the User object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldAdmin(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldAdmin is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldAdmin requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldAdmin: %w", err)
}
return oldValue.Admin, nil
}
// ResetAdmin resets all changes to the "admin" field.
func (m *UserMutation) ResetAdmin() {
m.admin = nil
}
// SetCreatedAt sets the "created_at" field.
func (m *UserMutation) SetCreatedAt(t time.Time) {
m.created_at = &t
@ -667,10 +838,10 @@ func (m *UserMutation) CreatedAt() (r time.Time, exists bool) {
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *UserMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) {
if !m.op.Is(OpUpdateOne) {
return v, fmt.Errorf("OldCreatedAt is only allowed on UpdateOne operations")
return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, fmt.Errorf("OldCreatedAt requires an ID field in the mutation")
return v, errors.New("OldCreatedAt requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
@ -743,11 +914,26 @@ func (m *UserMutation) Where(ps ...predicate.User) {
m.predicates = append(m.predicates, ps...)
}
// WhereP appends storage-level predicates to the UserMutation builder. Using this method,
// users can use type-assertion to append predicates that do not depend on any generated package.
func (m *UserMutation) WhereP(ps ...func(*sql.Selector)) {
p := make([]predicate.User, len(ps))
for i := range ps {
p[i] = ps[i]
}
m.Where(p...)
}
// Op returns the operation name.
func (m *UserMutation) Op() Op {
return m.op
}
// SetOp allows setting the mutation operation.
func (m *UserMutation) SetOp(op Op) {
m.op = op
}
// Type returns the node type of this mutation (User).
func (m *UserMutation) Type() string {
return m.typ
@ -757,7 +943,7 @@ func (m *UserMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *UserMutation) Fields() []string {
fields := make([]string, 0, 4)
fields := make([]string, 0, 6)
if m.name != nil {
fields = append(fields, user.FieldName)
}
@ -767,6 +953,12 @@ func (m *UserMutation) Fields() []string {
if m.password != nil {
fields = append(fields, user.FieldPassword)
}
if m.verified != nil {
fields = append(fields, user.FieldVerified)
}
if m.admin != nil {
fields = append(fields, user.FieldAdmin)
}
if m.created_at != nil {
fields = append(fields, user.FieldCreatedAt)
}
@ -784,6 +976,10 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) {
return m.Email()
case user.FieldPassword:
return m.Password()
case user.FieldVerified:
return m.Verified()
case user.FieldAdmin:
return m.Admin()
case user.FieldCreatedAt:
return m.CreatedAt()
}
@ -801,6 +997,10 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er
return m.OldEmail(ctx)
case user.FieldPassword:
return m.OldPassword(ctx)
case user.FieldVerified:
return m.OldVerified(ctx)
case user.FieldAdmin:
return m.OldAdmin(ctx)
case user.FieldCreatedAt:
return m.OldCreatedAt(ctx)
}
@ -833,6 +1033,20 @@ func (m *UserMutation) SetField(name string, value ent.Value) error {
}
m.SetPassword(v)
return nil
case user.FieldVerified:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetVerified(v)
return nil
case user.FieldAdmin:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetAdmin(v)
return nil
case user.FieldCreatedAt:
v, ok := value.(time.Time)
if !ok {
@ -898,6 +1112,12 @@ func (m *UserMutation) ResetField(name string) error {
case user.FieldPassword:
m.ResetPassword()
return nil
case user.FieldVerified:
m.ResetVerified()
return nil
case user.FieldAdmin:
m.ResetAdmin()
return nil
case user.FieldCreatedAt:
m.ResetCreatedAt()
return nil

View file

@ -1,15 +1,16 @@
// Code generated by entc, DO NOT EDIT.
// Code generated by ent, DO NOT EDIT.
package ent
import (
"fmt"
"goweb/ent/passwordtoken"
"goweb/ent/user"
"strings"
"time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"github.com/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/ent/user"
)
// PasswordToken is the model entity for the PasswordToken schema.
@ -17,14 +18,16 @@ type PasswordToken struct {
config `json:"-"`
// ID of the ent.
ID int `json:"id,omitempty"`
// Hash holds the value of the "hash" field.
Hash string `json:"-"`
// Token holds the value of the "token" field.
Token string `json:"-"`
// UserID holds the value of the "user_id" field.
UserID int `json:"user_id,omitempty"`
// CreatedAt holds the value of the "created_at" field.
CreatedAt time.Time `json:"created_at,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the PasswordTokenQuery when eager-loading is set.
Edges PasswordTokenEdges `json:"edges"`
password_token_user *int
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=<sensitive>")
builder.WriteString(", created_at=")
builder.WriteString(pt.CreatedAt.Format(time.ANSIC))
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
builder.WriteString("token=<sensitive>")
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
}
}

View file

@ -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/camzawacki/personal-site/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),
)
}

View file

@ -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/camzawacki/personal-site/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))
}

View file

@ -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/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/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 {
func (_c *PasswordTokenCreate) Save(ctx context.Context) (*PasswordToken, error) {
if err := _c.defaults(); 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
}
}
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 {
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?)")
}
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)}
}
}
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\"")}
_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)
}
}

View file

@ -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/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/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)
}
}

File diff suppressed because it is too large Load diff

View file

@ -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/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/ent/predicate"
"github.com/camzawacki/personal-site/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
}

View file

@ -1,4 +1,4 @@
// Code generated by entc, DO NOT EDIT.
// Code generated by ent, DO NOT EDIT.
package predicate

View file

@ -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/camzawacki/personal-site/ent/runtime/runtime.go

View file

@ -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/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/ent/schema"
"github.com/camzawacki/personal-site/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.
)

View file

@ -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/camzawacki/personal-site/ent"
"github.com/camzawacki/personal-site/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,
),
}
}

View file

@ -2,11 +2,13 @@ package schema
import (
"context"
"net/mail"
"strings"
"time"
ge "goweb/ent"
"goweb/ent/hook"
ge "github.com/camzawacki/personal-site/ent"
"github.com/camzawacki/personal-site/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)
})
},

View file

@ -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)
}

View file

@ -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/camzawacki/personal-site/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"`
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=<sensitive>")
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=<sensitive>")
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
}
}

View file

@ -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/camzawacki/personal-site/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),
)
}

View file

@ -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/camzawacki/personal-site/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))
}

View file

@ -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/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/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)
}
}

View file

@ -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/camzawacki/personal-site/ent/predicate"
"github.com/camzawacki/personal-site/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)
}
}

File diff suppressed because it is too large Load diff

View file

@ -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/camzawacki/personal-site/ent/passwordtoken"
"github.com/camzawacki/personal-site/ent/predicate"
"github.com/camzawacki/personal-site/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
}

View file

@ -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(`<a class="%s" href="%s">%s</a>`, strings.Join(classes, " "), url, text)
return template.HTML(html)
}

View file

@ -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 := `<a class="is-active" href="/abc">Text</a>`
assert.Equal(t, expected, link)
link = string(Link("/abc", "Text", "/abc", "first", "second"))
expected = `<a class="first second is-active" href="/abc">Text</a>`
assert.Equal(t, expected, link)
link = string(Link("/abc", "Text", "/def"))
expected = `<a class="" href="/abc">Text</a>`
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)
}

130
go.mod
View file

@ -1,78 +1,68 @@
module goweb
module github.com/camzawacki/personal-site
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
)

1084
go.sum

File diff suppressed because it is too large Load diff

View file

@ -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
}
}

64
main.go
View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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"))
}

View file

@ -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)
}
}
}

View file

@ -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))
}

62
pkg/context/context.go Normal file
View file

@ -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
}

View file

@ -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)
}

55
pkg/form/form.go Normal file
View file

@ -0,0 +1,55 @@
package form
import (
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/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)
}

67
pkg/form/form_test.go Normal file
View file

@ -0,0 +1,67 @@
package form
import (
"testing"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/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)
})
}

105
pkg/form/submission.go Normal file
View file

@ -0,0 +1,105 @@
package form
import (
"fmt"
"net/http"
"github.com/go-playground/validator/v10"
"github.com/camzawacki/personal-site/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)
}
}

View file

@ -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/camzawacki/personal-site/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)
})
}

198
pkg/handlers/admin.go Normal file
View file

@ -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/camzawacki/personal-site/ent"
"github.com/camzawacki/personal-site/ent/admin"
"github.com/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/pkg/middleware"
"github.com/camzawacki/personal-site/pkg/msg"
"github.com/camzawacki/personal-site/pkg/pager"
"github.com/camzawacki/personal-site/pkg/redirect"
"github.com/camzawacki/personal-site/pkg/routenames"
"github.com/camzawacki/personal-site/pkg/services"
"github.com/camzawacki/personal-site/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())
}
}

380
pkg/handlers/auth.go Normal file
View file

@ -0,0 +1,380 @@
package handlers
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/config"
"github.com/camzawacki/personal-site/ent"
"github.com/camzawacki/personal-site/ent/user"
"github.com/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/pkg/form"
"github.com/camzawacki/personal-site/pkg/log"
"github.com/camzawacki/personal-site/pkg/middleware"
"github.com/camzawacki/personal-site/pkg/msg"
"github.com/camzawacki/personal-site/pkg/redirect"
"github.com/camzawacki/personal-site/pkg/routenames"
"github.com/camzawacki/personal-site/pkg/services"
"github.com/camzawacki/personal-site/pkg/ui/emails"
"github.com/camzawacki/personal-site/pkg/ui/forms"
"github.com/camzawacki/personal-site/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()
}

76
pkg/handlers/cache.go Normal file
View file

@ -0,0 +1,76 @@
package handlers
import (
"errors"
"time"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/pkg/form"
"github.com/camzawacki/personal-site/pkg/routenames"
"github.com/camzawacki/personal-site/pkg/services"
"github.com/camzawacki/personal-site/pkg/ui/forms"
"github.com/camzawacki/personal-site/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)
}

62
pkg/handlers/contact.go Normal file
View file

@ -0,0 +1,62 @@
package handlers
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/pkg/form"
"github.com/camzawacki/personal-site/pkg/routenames"
"github.com/camzawacki/personal-site/pkg/services"
"github.com/camzawacki/personal-site/pkg/ui/forms"
"github.com/camzawacki/personal-site/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)
}

43
pkg/handlers/error.go Normal file
View file

@ -0,0 +1,43 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/pkg/log"
"github.com/camzawacki/personal-site/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,
)
}
}

80
pkg/handlers/files.go Normal file
View file

@ -0,0 +1,80 @@
package handlers
import (
"fmt"
"io"
"time"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/pkg/msg"
"github.com/camzawacki/personal-site/pkg/routenames"
"github.com/camzawacki/personal-site/pkg/services"
"github.com/camzawacki/personal-site/pkg/ui/models"
"github.com/camzawacki/personal-site/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)
}

36
pkg/handlers/handlers.go Normal file
View file

@ -0,0 +1,36 @@
package handlers
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/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))
}

View file

@ -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)
}

55
pkg/handlers/pages.go Normal file
View file

@ -0,0 +1,55 @@
package handlers
import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/pkg/pager"
"github.com/camzawacki/personal-site/pkg/routenames"
"github.com/camzawacki/personal-site/pkg/services"
"github.com/camzawacki/personal-site/pkg/ui/models"
"github.com/camzawacki/personal-site/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)
}

View file

@ -1,23 +1,24 @@
package routes
package handlers
import (
"net/http"
"testing"
"github.com/camzawacki/personal-site/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())
}

93
pkg/handlers/router.go Normal file
View file

@ -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/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/pkg/middleware"
"github.com/camzawacki/personal-site/pkg/services"
files "github.com/camzawacki/personal-site/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
}

View file

@ -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/camzawacki/personal-site/config"
"github.com/camzawacki/personal-site/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,
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
}

44
pkg/handlers/search.go Normal file
View file

@ -0,0 +1,44 @@
package handlers
import (
"fmt"
"math/rand"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/pkg/routenames"
"github.com/camzawacki/personal-site/pkg/services"
"github.com/camzawacki/personal-site/pkg/ui/models"
"github.com/camzawacki/personal-site/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)
}

71
pkg/handlers/task.go Normal file
View file

@ -0,0 +1,71 @@
package handlers
import (
"fmt"
"time"
"github.com/mikestefanello/backlite"
"github.com/camzawacki/personal-site/pkg/msg"
"github.com/camzawacki/personal-site/pkg/routenames"
"github.com/camzawacki/personal-site/pkg/ui/forms"
"github.com/camzawacki/personal-site/pkg/ui/pages"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/pkg/form"
"github.com/camzawacki/personal-site/pkg/services"
"github.com/camzawacki/personal-site/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)
}

97
pkg/htmx/htmx.go Normal file
View file

@ -0,0 +1,97 @@
package htmx
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/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
}
}

View file

@ -4,7 +4,8 @@ import (
"net/http"
"testing"
"goweb/tests"
"github.com/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/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)
}

27
pkg/log/log.go Normal file
View file

@ -0,0 +1,27 @@
package log
import (
"log/slog"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/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()
}

21
pkg/log/log_test.go Normal file
View file

@ -0,0 +1,21 @@
package log
import (
"testing"
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/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)
}

120
pkg/middleware/auth.go Normal file
View file

@ -0,0 +1,120 @@
package middleware
import (
"fmt"
"net/http"
"strconv"
"github.com/camzawacki/personal-site/ent"
"github.com/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/pkg/log"
"github.com/camzawacki/personal-site/pkg/msg"
"github.com/camzawacki/personal-site/pkg/routenames"
"github.com/camzawacki/personal-site/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)
}
}

View file

@ -1,13 +1,14 @@
package middleware
import (
goctx "context"
"fmt"
"net/http"
"testing"
"goweb/context"
"goweb/ent"
"goweb/tests"
"github.com/camzawacki/personal-site/ent"
"github.com/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/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)
}

22
pkg/middleware/cache.go Normal file
View file

@ -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)
}
}
}

View file

@ -0,0 +1,18 @@
package middleware
import (
"testing"
"time"
"github.com/camzawacki/personal-site/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"))
}

17
pkg/middleware/config.go Normal file
View file

@ -0,0 +1,17 @@
package middleware
import (
"github.com/labstack/echo/v4"
"github.com/camzawacki/personal-site/config"
"github.com/camzawacki/personal-site/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)
}
}
}

View file

@ -0,0 +1,22 @@
package middleware
import (
"testing"
"github.com/camzawacki/personal-site/config"
"github.com/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/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)
}

View file

@ -1,17 +1,18 @@
package middleware
import (
"fmt"
"net/http"
"strconv"
"goweb/context"
"goweb/ent"
"goweb/ent/user"
"github.com/camzawacki/personal-site/ent"
"github.com/camzawacki/personal-site/ent/user"
"github.com/camzawacki/personal-site/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),
)
}
}
}

View file

@ -4,9 +4,9 @@ import (
"fmt"
"testing"
"goweb/context"
"goweb/ent"
"goweb/tests"
"github.com/camzawacki/personal-site/ent"
"github.com/camzawacki/personal-site/pkg/context"
"github.com/camzawacki/personal-site/pkg/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Some files were not shown because too many files have changed in this diff Show more