├── .air.toml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── app
├── account_pages.go
├── api.go
├── config.go
├── email.go
├── errors.go
├── gen
│ └── models
│ │ ├── client.go
│ │ ├── config.go
│ │ ├── context.go
│ │ ├── ent.go
│ │ ├── enttest
│ │ └── enttest.go
│ │ ├── hook
│ │ └── hook.go
│ │ ├── migrate
│ │ ├── migrate.go
│ │ └── schema.go
│ │ ├── mutation.go
│ │ ├── predicate
│ │ └── predicate.go
│ │ ├── runtime.go
│ │ ├── runtime
│ │ └── runtime.go
│ │ ├── task.go
│ │ ├── task
│ │ ├── task.go
│ │ └── where.go
│ │ ├── task_create.go
│ │ ├── task_delete.go
│ │ ├── task_query.go
│ │ ├── task_update.go
│ │ └── tx.go
├── generator
│ ├── entc.go
│ └── generate.go
├── router.go
├── schema
│ └── task.go
├── stripe.go
├── task_pages.go
└── webhook.go
├── assets
├── .babelrc
├── images
│ └── logo.svg
├── package-lock.json
├── package.json
├── src
│ ├── components
│ │ ├── App.svelte
│ │ ├── Groups.svelte
│ │ ├── Guests.svelte
│ │ ├── Members.svelte
│ │ └── index.js
│ ├── controllers
│ │ ├── account_controller.js
│ │ ├── clipboard_controller.js
│ │ ├── hover_hidden_controller.js
│ │ ├── navigate_controller.js
│ │ ├── subscription_controller.js
│ │ ├── svelte_controller.js
│ │ ├── tabs_controller.js
│ │ └── toggle_controller.js
│ ├── index.js
│ └── styles.scss
├── webpack.config.js
└── yarn.lock
├── env.development
├── feature_groups.development.json
├── go.mod
├── go.sum
├── main.go
├── plans.development.json
├── stripe.md
└── templates
├── 404.html
├── account
├── changed.html
├── confirmed.html
├── forgot.html
├── login.html
├── magic.html
├── main.html
├── main_workspace.html
├── reset.html
└── signup.html
├── app.html
├── home.html
├── layouts
├── empty.html
└── index.html
└── partials
├── developer.html
├── errors.html
├── footer.html
├── groups.html
├── guests.html
├── header.html
├── list_members.html
├── members.html
├── navbar.html
├── plan.html
├── plans.html
├── profile.html
└── workspace_switcher.html
/.air.toml:
--------------------------------------------------------------------------------
1 | # Config file for [Air](https://github.com/cosmtrek/air) in TOML format
2 |
3 | # Working directory
4 | # . or absolute path, please note that the directories following must be under root.
5 | root = "."
6 | tmp_dir = "tmp"
7 |
8 | [build]
9 | # Just plain old shell command. You could use `make` as well.
10 | cmd = "go build -o ./tmp/main main.go"
11 | # Binary file yields from `cmd`.
12 | bin = "tmp/main"
13 | # Customize binary.
14 | full_bin = "APP_ENV=dev APP_USER=air ./tmp/main -config env.local"
15 | # Watch these filename extensions.
16 | include_ext = ["go", "tpl", "tmpl", "html"]
17 | # Ignore these filename extensions or directories.
18 | exclude_dir = ["web", "screenshots", "vendor"]
19 | # Watch these directories if you specified.
20 | include_dir = ["app","tasks"]
21 | # Exclude files.
22 | exclude_file = ["tasks.db",".gitignore"]
23 | # Exclude unchanged files.
24 | exclude_unchanged = true
25 | # This log file places in your tmp_dir.
26 | log = "air.log"
27 | # It's not necessary to trigger build each time file changes if it's too frequent.
28 | delay = 1000 # ms
29 | # Stop running old binary when build errors occur.
30 | stop_on_error = true
31 | # Send Interrupt signal before killing process (windows does not support this feature)
32 | send_interrupt = false
33 | # Delay after sending Interrupt signal
34 | kill_delay = 500 # ms
35 |
36 | [log]
37 | # Show log time
38 | time = false
39 |
40 | [color]
41 | # Customize each part's color. If no color found, use the raw app log.
42 | main = "magenta"
43 | watcher = "cyan"
44 | build = "yellow"
45 | runner = "green"
46 |
47 | [misc]
48 | # Delete tmp directory on exit
49 | clean_on_exit = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | assets/node_modules
3 | .idea
4 | *.db
5 | tmp
6 | .DS_Store
7 | bin
8 | *.local
9 | *.local.*
10 | public
11 | ops/build
12 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.15.5-buster as build-go
2 | WORKDIR /go/src/app
3 | COPY . .
4 | RUN CGO_ENABLED=1 GOOS=linux go build -a -ldflags '-linkmode external -extldflags "-static"' -o main .
5 |
6 | FROM node:14.3.0-stretch as build-node
7 | WORKDIR /usr/src/app
8 | COPY . .
9 | RUN cd assets && npm install
10 | RUN cd assets && npm run build
11 |
12 | FROM alpine:latest
13 | RUN apk add ca-certificates curl
14 | WORKDIR /opt
15 | COPY --from=build-go /go/src/app/ /bin
16 | RUN chmod +x /bin/main
17 | COPY --from=build-node /usr/src/app/public /opt/public
18 | COPY --from=build-node /usr/src/app/templates /opt/templates
19 | CMD ["/bin/main"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Adnaan Badr
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | go get -v ./... && cd assets && yarn install
3 | watch: install
4 | echo "watching go files and assets directory..."; \
5 | air -d -c .air.toml & \
6 | cd assets && yarn watch & \
7 | wait; \
8 | echo "bye!"
9 | watch-go:
10 | air -c .air.toml
11 | run-go:
12 | go run main.go -config env.local
13 | watch-assets:
14 | cd assets && yarn watch
15 | build-docker:
16 | docker build -t gomodest .
17 | run-docker:
18 | docker run -it --rm -p 4000:4000 --env-file env.local gomodest:latest
19 | mailhog:
20 | docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
21 | generate-models:
22 | go generate ./app/generator
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **GOMODEST** is a Multi Page App(MPA) **starter kit** using Go's `html/template`, `SvelteJS` and `StimulusJS`. It is inspired from modest approaches to building webapps as enlisted in https://modestjs.works/.
2 |
3 | ## Motivation
4 |
5 | I am a devops engineer who dabbles in UI for side projects, internal tools and such. The SPA/ReactJS ecosystem is too costly for me. This is an alternative approach.
6 |
7 |
8 | > The main idea is to use server rendered html with spots of client-side dynamism using SvelteJS & StimulusJS
9 |
10 | The webapp is mostly plain old javascript, html, css but with sprinkles of StimulusJS & spots of SvelteJS used for interactivity sans page reloads. StimulusJS is used for sprinkling
11 | interactivity in server rendered html & mounting Svelte components into divs.
12 |
13 | ## Stack
14 |
15 | A few things which were used:
16 |
17 | 1. Go, html/template, goview,
18 | 2. Authentication: [github.com/adnaan/authn](https://github.com/adnaan/authn)
19 | 3. SvelteJS
20 | 4. [Hotwire](https://hotwire.dev/)
21 | 5. Bulma CSS
22 |
23 | Many more things in `go.mod` & `web/package.json`
24 |
25 | To run, clone this repo and:
26 |
27 | ```bash
28 | $ make install
29 | # another terminal
30 | $ make run-go
31 | ```
32 |
33 | The ideas in this starter kit follow the JS gradient as noted [here](https://modestjs.works/book/part-2/the-js-gradient/). I have taken the liberty to organise them into the following big blocks: **server-rendered html**, **sprinkles** and **spots**.
34 |
35 | ## Server Rendered HTML
36 |
37 | Use `html/template` and `goview` to render html pages. It's quite powerful when do you don't need client-side interactions.
38 |
39 | example:
40 |
41 | ```go
42 | func accountPage(w http.ResponseWriter, r *http.Request) (goview.M, error) {
43 |
44 | session, err := store.Get(r, "auth-session")
45 | if err != nil {
46 | return nil, fmt.Errorf("%v, %w", err, InternalErr)
47 | }
48 |
49 | profileData, ok := session.Values["profile"]
50 | if !ok {
51 | return nil, fmt.Errorf("%v, %w", err, InternalErr)
52 | }
53 |
54 | profile, ok := profileData.(map[string]interface{})
55 | if !ok {
56 | return nil, fmt.Errorf("%v, %w", err, InternalErr)
57 | }
58 |
59 | return goview.M{
60 | "name": profile["name"],
61 | }, nil
62 |
63 | }
64 | ```
65 |
66 | ## Sprinkles
67 |
68 | Use `stimulusjs` to level up server-rendered html to handle simple interactions like: navigations, form validations etc.
69 |
70 | example:
71 |
72 | ```html
73 |
77 | Home
78 |
79 | ```
80 |
81 | ```js
82 | goto(e){
83 | if (e.currentTarget.dataset.goto){
84 | window.location = e.currentTarget.dataset.goto;
85 | }
86 | }
87 | ```
88 |
89 | ## Spots
90 |
91 | Use `sveltejs` to take over `spots` of a server-rendered html page to provide more complex interactivity without page reloads.
92 |
93 | This snippet is the most interesting part of this project:
94 |
95 | ```html
96 | {{define "content"}}
97 |
106 |
107 | {{end}}
108 | ```
109 |
110 | [source](https://github.com/adnaan/gomodest-starter/blob/main/web/html/app.html)
111 |
112 | In the above snippet, we use StimulusJS to mount a Svelte component by using the following code:
113 |
114 | ```js
115 | import { Controller } from "stimulus";
116 | import components from "./components";
117 |
118 | export default class extends Controller {
119 | static targets = ["component"]
120 | connect() {
121 | if (this.componentTargets.length > 0){
122 | this.componentTargets.forEach(el => {
123 | const componentName = el.dataset.componentName;
124 | const componentProps = el.dataset.componentProps ? JSON.parse(el.dataset.componentProps): {};
125 | if (!(componentName in components)){
126 | console.log(`svelte component: ${componentName}, not found!`)
127 | return;
128 | }
129 | const app = new components[componentName]({
130 | target: el,
131 | props: componentProps
132 | });
133 | })
134 | }
135 | }
136 | }
137 | ```
138 | [source](https://github.com/adnaan/gomodest-starter/blob/main/web/src/controllers/svelte_controller.js)
139 |
140 | This strategy allows us to mix server rendered HTML pages with client side dynamism.
141 |
142 | Other possibly interesting aspects could be the layout of [web/html](https://github.com/adnaan/gomodest-starter/tree/main/web/html) and the usage of the super nice [goview](https://github.com/foolin/goview) library to render html in these files:
143 |
144 | 1. [router.go](https://github.com/adnaan/gomodest-starter/blob/main/routes/router.go)
145 | 2. [view.go](https://github.com/adnaan/gomodest-starter/blob/main/routes/view.go)
146 | 3. [pages.go](https://github.com/adnaan/gomodest-starter/blob/main/routes/pages.go)
147 |
148 | That is all.
149 |
150 | ---------------
151 |
152 |
153 | ## Attributions
154 |
155 | 1. https://areknawo.com/making-a-todo-app-in-svelte/
156 | 2. https://modestjs.works/
157 | 3. https://github.com/foolin/goview
158 |
159 |
160 |
161 |
--------------------------------------------------------------------------------
/app/account_pages.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "strings"
8 |
9 | "github.com/lithammer/shortuuid/v3"
10 |
11 | rl "github.com/adnaan/renderlayout"
12 |
13 | "github.com/stripe/stripe-go/v72"
14 | "github.com/stripe/stripe-go/v72/sub"
15 |
16 | "github.com/google/uuid"
17 |
18 | "github.com/go-chi/chi"
19 | )
20 |
21 | func defaultPageHandler(appCtx Context) rl.Data {
22 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
23 | pageData := map[string]interface{}{}
24 | pageData["route"] = r.URL.Path
25 | pageData["app_name"] = strings.Title(strings.ToLower(appCtx.cfg.Name))
26 | pageData["feature_groups"] = appCtx.cfg.FeatureGroups
27 |
28 | account, err := appCtx.authn.CurrentAccount(r)
29 | if err != nil {
30 | return pageData, nil
31 | }
32 |
33 | accAttributes := account.Attributes().Map()
34 | if _, ok := accAttributes["api_key"]; ok {
35 | pageData["is_api_token_set"] = true
36 | }
37 |
38 | pageData["is_logged_in"] = true
39 | pageData["email"] = account.Email()
40 | pageData["metadata"] = accAttributes
41 |
42 | currentPriceID, _ := account.Attributes().Session().Get(currentPriceIDKey)
43 | // get currentPriceID using stripe customer ID
44 | billingId, billingIDExists := accAttributes.String(billingIDKey)
45 | if billingIDExists && currentPriceID == nil {
46 | params := &stripe.SubscriptionListParams{
47 | Customer: billingId,
48 | Status: string(stripe.SubscriptionStatusActive),
49 | }
50 | params.AddExpand("data.items.data.price")
51 | params.Filters.AddFilter("limit", "", "1")
52 |
53 | i := sub.List(params)
54 | for i.Next() {
55 | s := i.Subscription()
56 | if s.Status == stripe.SubscriptionStatusActive {
57 | for _, pr := range s.Items.Data {
58 | currentPriceID = pr.Price.ID
59 | }
60 | }
61 |
62 | }
63 | }
64 |
65 | if currentPriceID != nil {
66 | err = account.Attributes().Session().Set(w, currentPriceIDKey, currentPriceID)
67 | if err != nil {
68 | log.Println("SetSessionVal", err)
69 | }
70 |
71 | for _, plan := range appCtx.cfg.Plans {
72 | if plan.PriceID == currentPriceID.(string) {
73 | pageData["current_plan"] = Plan{
74 | Current: true,
75 | PriceID: plan.PriceID,
76 | Name: plan.Name,
77 | Price: plan.Price,
78 | Details: plan.Details,
79 | StripeKey: plan.StripeKey,
80 | }
81 | }
82 | }
83 |
84 | }
85 |
86 | return pageData, nil
87 | }
88 | }
89 |
90 | func signupPage(appCtx Context) rl.Data {
91 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
92 | var email, password string
93 | metadata := make(map[string]interface{})
94 | _ = r.ParseForm()
95 | for k, v := range r.Form {
96 |
97 | if k == "email" && len(v) == 0 {
98 | return rl.D{}, fmt.Errorf("email is required")
99 | }
100 |
101 | if k == "password" && len(v) == 0 {
102 | return rl.D{}, fmt.Errorf("password is required")
103 | }
104 |
105 | if len(v) == 0 {
106 | continue
107 | }
108 |
109 | if k == "email" && len(v) > 0 {
110 | email = v[0]
111 | continue
112 | }
113 |
114 | if k == "password" && len(v) > 0 {
115 | password = v[0]
116 | continue
117 | }
118 |
119 | if len(v) == 1 {
120 | metadata[k] = v[0]
121 | continue
122 | }
123 | if len(v) > 1 {
124 | metadata[k] = v
125 | }
126 | }
127 |
128 | err := appCtx.authn.Signup(r.Context(), email, password, metadata)
129 | if err != nil {
130 | return rl.D{}, err
131 | }
132 |
133 | http.Redirect(w, r, "/login?confirmation_sent=true", http.StatusSeeOther)
134 |
135 | return rl.D{}, nil
136 | }
137 | }
138 |
139 | func loginPage(appCtx Context) rl.Data {
140 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
141 |
142 | confirmed := r.URL.Query().Get("confirmed")
143 | if confirmed == "true" {
144 | return rl.D{
145 | "confirmed": true,
146 | }, nil
147 | }
148 |
149 | notConfirmed := r.URL.Query().Get("not_confirmed")
150 | if notConfirmed == "true" {
151 | return rl.D{
152 | "not_confirmed": true,
153 | }, nil
154 | }
155 |
156 | confirmationSent := r.URL.Query().Get("confirmation_sent")
157 | if confirmationSent == "true" {
158 | return rl.D{
159 | "confirmation_sent": true,
160 | }, nil
161 | }
162 |
163 | emailChanged := r.URL.Query().Get("email_changed")
164 | if emailChanged == "true" {
165 | return rl.D{
166 | "email_changed": true,
167 | }, nil
168 | }
169 |
170 | return rl.D{}, nil
171 | }
172 | }
173 |
174 | func loginPageSubmit(appCtx Context) rl.Data {
175 | type req struct {
176 | Email *string
177 | Password *string
178 | Magic *string
179 | }
180 |
181 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
182 | form := new(req)
183 | err := r.ParseForm()
184 | if err != nil {
185 | return nil, fmt.Errorf("%w", err)
186 | }
187 |
188 | err = appCtx.formDecoder.Decode(form, r.Form)
189 | if err != nil {
190 | return nil, fmt.Errorf("%w", err)
191 | }
192 |
193 | if form.Email == nil {
194 | return nil, fmt.Errorf("%w", fmt.Errorf("email is empty"))
195 | }
196 |
197 | if form.Magic != nil && *form.Magic == "magic" {
198 | err := appCtx.authn.SendPasswordlessToken(r.Context(), *form.Email)
199 | if err != nil {
200 | return nil, err
201 | }
202 | http.Redirect(w, r, "/magic-link-sent", http.StatusSeeOther)
203 | } else {
204 | if form.Password == nil {
205 | return nil, fmt.Errorf("%w", fmt.Errorf("password is empty"))
206 | }
207 | err := appCtx.authn.Login(w, r, *form.Email, *form.Password)
208 | if err != nil {
209 | return nil, err
210 | }
211 |
212 | redirectTo := "/app"
213 | from := r.URL.Query().Get("from")
214 | if from != "" {
215 | redirectTo = from
216 | }
217 |
218 | http.Redirect(w, r, redirectTo, http.StatusSeeOther)
219 | }
220 |
221 | return rl.D{}, nil
222 | }
223 | }
224 |
225 | func magicLinkLoginConfirm(appCtx Context) rl.Data {
226 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
227 | otp := chi.URLParam(r, "otp")
228 | err := appCtx.authn.LoginWithPasswordlessToken(w, r, otp)
229 | if err != nil {
230 | return nil, err
231 | }
232 |
233 | redirectTo := "/app"
234 |
235 | http.Redirect(w, r, redirectTo, http.StatusSeeOther)
236 |
237 | return rl.D{}, nil
238 | }
239 | }
240 |
241 | func loginProviderCallbackPage(appCtx Context) rl.Data {
242 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
243 | err := appCtx.authn.LoginProviderCallback(w, r, nil)
244 | if err != nil {
245 | return rl.D{}, err
246 | }
247 | redirectTo := "/app"
248 |
249 | http.Redirect(w, r, redirectTo, http.StatusSeeOther)
250 | return rl.D{}, nil
251 | }
252 | }
253 |
254 | func loginProviderPage(appCtx Context) rl.Data {
255 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
256 | err := appCtx.authn.LoginWithProvider(w, r)
257 | if err != nil {
258 | return rl.D{}, err
259 | }
260 | redirectTo := "/app"
261 | from := r.URL.Query().Get("from")
262 | if from != "" {
263 | redirectTo = from
264 | }
265 |
266 | http.Redirect(w, r, redirectTo, http.StatusSeeOther)
267 | return rl.D{}, nil
268 | }
269 | }
270 |
271 | func confirmEmailChangePage(appCtx Context) rl.Data {
272 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
273 | token := chi.URLParam(r, "token")
274 | acc, err := appCtx.authn.CurrentAccount(r)
275 | if err != nil {
276 | return nil, err
277 | }
278 | err = acc.ConfirmEmailChange(token)
279 | if err != nil {
280 | return nil, err
281 | }
282 | http.Redirect(w, r, "/account?email_changed=true", http.StatusSeeOther)
283 | return rl.D{}, nil
284 | }
285 | }
286 |
287 | func confirmEmailPage(appCtx Context) rl.Data {
288 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
289 | token := chi.URLParam(r, "token")
290 | err := appCtx.authn.ConfirmSignupEmail(r.Context(), token)
291 | if err != nil {
292 | return nil, err
293 | }
294 |
295 | http.Redirect(w, r, "/login?confirmed=true", http.StatusSeeOther)
296 | return rl.D{}, nil
297 | }
298 | }
299 |
300 | func forgotPage(appCtx Context) rl.Data {
301 | type req struct {
302 | Email *string
303 | }
304 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
305 | form := new(req)
306 | err := r.ParseForm()
307 | if err != nil {
308 | return nil, fmt.Errorf("%w", err)
309 | }
310 |
311 | err = appCtx.formDecoder.Decode(form, r.Form)
312 | if err != nil {
313 | return nil, fmt.Errorf("%w", err)
314 | }
315 |
316 | if form.Email == nil {
317 | return nil, fmt.Errorf("%w", fmt.Errorf("email is empty"))
318 | }
319 |
320 | pageData := make(map[string]interface{})
321 |
322 | err = appCtx.authn.Recovery(r.Context(), *form.Email)
323 | if err != nil {
324 | return pageData, err
325 | }
326 |
327 | pageData["recovery_sent"] = true
328 |
329 | return pageData, nil
330 | }
331 | }
332 |
333 | func resetPage(appCtx Context) rl.Data {
334 | type req struct {
335 | Password *string
336 | }
337 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
338 | token := chi.URLParam(r, "token")
339 | form := new(req)
340 | err := r.ParseForm()
341 | if err != nil {
342 | return nil, fmt.Errorf("%w", err)
343 | }
344 |
345 | err = appCtx.formDecoder.Decode(form, r.Form)
346 | if err != nil {
347 | return nil, fmt.Errorf("%w", err)
348 | }
349 |
350 | if form.Password == nil {
351 | return nil, fmt.Errorf("%w", fmt.Errorf("password is empty"))
352 | }
353 |
354 | err = appCtx.authn.ConfirmRecovery(r.Context(), token, *form.Password)
355 | if err != nil {
356 | return rl.D{}, err
357 | }
358 |
359 | http.Redirect(w, r, "/login", http.StatusSeeOther)
360 |
361 | return rl.D{}, nil
362 | }
363 | }
364 |
365 | func accountPage(appCtx Context) rl.Data {
366 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
367 | emailChanged := r.URL.Query().Get("email_changed")
368 | if emailChanged == "true" {
369 | return rl.D{
370 | "form_token": uuid.New(),
371 | "email_changed": true,
372 | }, nil
373 | }
374 |
375 | checkout := r.URL.Query().Get("checkout")
376 | if checkout == "success" || checkout == "cancel" {
377 | return rl.D{
378 | "checkout": checkout,
379 | "plans": appCtx.cfg.Plans,
380 | }, nil
381 | }
382 |
383 | return rl.D{
384 | "form_token": uuid.New(),
385 | "plans": appCtx.cfg.Plans,
386 | }, nil
387 | }
388 | }
389 |
390 | func accountPageSubmit(appCtx Context) rl.Data {
391 | type req struct {
392 | Name *string
393 | Email *string
394 | ResetAPIToken bool
395 | FormToken *string
396 | }
397 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
398 | form := new(req)
399 | err := r.ParseForm()
400 | if err != nil {
401 | return nil, fmt.Errorf("%w", err)
402 | }
403 |
404 | err = appCtx.formDecoder.Decode(form, r.Form)
405 | if err != nil {
406 | return nil, fmt.Errorf("%w", err)
407 | }
408 |
409 | pageData := make(map[string]interface{})
410 |
411 | account, err := appCtx.authn.CurrentAccount(r)
412 | if err != nil {
413 | return nil, err
414 | }
415 |
416 | if form.ResetAPIToken {
417 | // check if the form has been previously submitted
418 | if form.FormToken != nil {
419 | formTokenVal, err := account.Attributes().Session().Get("form_token")
420 | if err == nil && formTokenVal != nil {
421 | formToken := formTokenVal.(string)
422 | if formToken == *form.FormToken {
423 | return rl.D{}, nil
424 | }
425 | }
426 | }
427 |
428 | apiKey := shortuuid.New()
429 | token, err := appCtx.branca.EncodeToString(apiKey)
430 | if err != nil {
431 | return nil, fmt.Errorf("%v %w", err, ErrInternal)
432 | }
433 |
434 | err = account.Attributes().Set("api_key", apiKey)
435 | if err != nil {
436 | return nil, fmt.Errorf("%v %w", err, ErrInternal)
437 | }
438 |
439 | account.Attributes().Session().Set(w, "form_token", form.FormToken)
440 | return rl.D{
441 | "is_api_token_set": true,
442 | "api_token": token,
443 | }, nil
444 | }
445 |
446 | if form.Email != nil && *form.Email != account.Email() {
447 | err = account.ChangeEmail(*form.Email)
448 | if err != nil {
449 | return nil, err
450 | }
451 | pageData["change_email"] = "requested"
452 | }
453 |
454 | name, _ := account.Attributes().Map().String("name")
455 | if name != *form.Name {
456 | err = account.Attributes().Set("name", form.Name)
457 | if err != nil {
458 | return nil, err
459 | }
460 | }
461 |
462 | account.Attributes().Session().Set(w, "form_token", form.FormToken)
463 |
464 | pageData["email"] = account.Email()
465 | pageData["metadata"] = account.Attributes().Map()
466 | return pageData, nil
467 | }
468 | }
469 |
470 | func deleteAccount(appCtx Context) rl.Data {
471 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
472 | account, err := appCtx.authn.CurrentAccount(r)
473 | if err != nil {
474 | return nil, err
475 | }
476 | err = account.Delete()
477 | if err != nil {
478 | return nil, err
479 | }
480 | http.Redirect(w, r, "/", http.StatusSeeOther)
481 | return rl.D{}, nil
482 | }
483 | }
484 |
--------------------------------------------------------------------------------
/app/api.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/go-chi/chi"
8 |
9 | "github.com/lithammer/shortuuid/v3"
10 |
11 | "github.com/go-chi/render"
12 |
13 | "github.com/adnaan/authn"
14 | "github.com/adnaan/gomodest-starter/app/gen/models/task"
15 | )
16 |
17 | func list(t Context) http.HandlerFunc {
18 | return func(w http.ResponseWriter, r *http.Request) {
19 | userID := authn.AccountIDFromContext(r)
20 | tasks, err := t.db.Task.Query().Where(task.Owner(userID)).All(r.Context())
21 | if err != nil {
22 | render.Render(w, r, ErrInternal(err))
23 | return
24 | }
25 |
26 | render.JSON(w, r, tasks)
27 | }
28 | }
29 |
30 | func create(t Context) http.HandlerFunc {
31 | type req struct {
32 | Text string `json:"text"`
33 | }
34 | return func(w http.ResponseWriter, r *http.Request) {
35 | req := new(req)
36 | userID := authn.AccountIDFromContext(r)
37 | err := render.DecodeJSON(r.Body, &req)
38 | if err != nil {
39 | render.Render(w, r, ErrInternal(err))
40 | return
41 | }
42 |
43 | newTask, err := t.db.Task.Create().
44 | SetID(shortuuid.New()).
45 | SetStatus(task.StatusInprogress).
46 | SetOwner(userID).
47 | SetText(req.Text).
48 | Save(r.Context())
49 | if err != nil {
50 | render.Render(w, r, ErrInternal(err))
51 | return
52 | }
53 | render.JSON(w, r, newTask)
54 | }
55 | }
56 |
57 | func updateStatus(t Context) http.HandlerFunc {
58 | type req struct {
59 | Status string `json:"status"`
60 | }
61 | return func(w http.ResponseWriter, r *http.Request) {
62 | req := new(req)
63 | id := chi.URLParam(r, "id")
64 |
65 | err := render.DecodeJSON(r.Body, &req)
66 | if err != nil {
67 | render.Render(w, r, ErrInternal(err))
68 | return
69 | }
70 |
71 | updatedTask, err := t.db.Task.
72 | UpdateOneID(id).
73 | SetUpdatedAt(time.Now()).
74 | SetStatus(task.Status(req.Status)).
75 | Save(r.Context())
76 | if err != nil {
77 | render.Render(w, r, ErrInternal(err))
78 | return
79 | }
80 | render.JSON(w, r, updatedTask)
81 | }
82 | }
83 |
84 | func updateText(t Context) http.HandlerFunc {
85 | type req struct {
86 | Text string `json:"text"`
87 | }
88 | return func(w http.ResponseWriter, r *http.Request) {
89 | req := new(req)
90 | id := chi.URLParam(r, "id")
91 |
92 | err := render.DecodeJSON(r.Body, &req)
93 | if err != nil {
94 | render.Render(w, r, ErrInternal(err))
95 | return
96 | }
97 |
98 | updatedTask, err := t.db.Task.
99 | UpdateOneID(id).
100 | SetUpdatedAt(time.Now()).
101 | SetText(req.Text).
102 | Save(r.Context())
103 | if err != nil {
104 | render.Render(w, r, ErrInternal(err))
105 | return
106 | }
107 | render.JSON(w, r, updatedTask)
108 | }
109 | }
110 |
111 | func delete(t Context) http.HandlerFunc {
112 | return func(w http.ResponseWriter, r *http.Request) {
113 | id := chi.URLParam(r, "id")
114 | err := t.db.Task.DeleteOneID(id).Exec(r.Context())
115 | if err != nil {
116 | render.Render(w, r, ErrInternal(err))
117 | return
118 | }
119 | render.Status(r, http.StatusOK)
120 | render.JSON(w, r, struct {
121 | Success bool `json:"success"`
122 | }{
123 | Success: true,
124 | })
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/app/config.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "os"
9 |
10 | "github.com/joho/godotenv"
11 | "github.com/kelseyhightower/envconfig"
12 | )
13 |
14 | type Config struct {
15 | Name string `json:"name" default:"gomodest"`
16 | Domain string `json:"domain" default:"https://gomodest.xyz"`
17 | Port int `json:"port" default:"4000"`
18 | HealthPath string `json:"health_path" envconfig:"health_path" default:"/healthz"`
19 | ReadTimeoutSecs int `json:"read_timeout_secs" envconfig:"read_timeout_secs" default:"5"`
20 | WriteTimeoutSecs int `json:"write_timeout_secs" envconfig:"write_timeout_secs" default:"10"`
21 | LogLevel string `json:"log_level" envconfig:"log_level" default:"error"`
22 | LogFormatJSON bool `json:"log_format_json" envconfig:"log_format_json" default:"false"`
23 | Templates string `json:"templates" envconfig:"templates" default:"templates"`
24 | SessionSecret string `json:"session_secret" envconfig:"session_secret" default:"mysessionsecret"`
25 | APIMasterSecret string `json:"api_master_secret" envconfig:"api_master_secret" default:"supersecretkeyyoushouldnotcommit"`
26 |
27 | // datasource
28 | Driver string `json:"driver" envconfig:"driver" default:"sqlite3"`
29 | DataSource string `json:"datasource" envconfig:"datasource" default:"file:gomodest.db?mode=memory&cache=shared&_fk=1"`
30 |
31 | // smtp
32 | SMTPHost string `json:"smtp_host" envconfig:"smtp_host" default:"0.0.0.0"`
33 | SMTPPort int `json:"smtp_port,omitempty" envconfig:"smtp_port" default:"1025"`
34 | SMTPUser string `json:"smtp_user" envconfig:"smtp_user" default:"myuser" `
35 | SMTPPass string `json:"smtp_pass,omitempty" envconfig:"smtp_pass" default:"mypass"`
36 | SMTPAdminEmail string `json:"smtp_admin_email" envconfig:"smtp_admin_email" default:"noreply@gomodest.xyz"`
37 | SMTPDebug bool `json:"smtp_debug" envconfig:"smtp_debug" default:"true"`
38 |
39 | // goth
40 | GoogleClientID string `json:"google_client_id" envconfig:"google_client_id"`
41 | GoogleSecret string `json:"google_secret" envconfig:"google_secret"`
42 |
43 | // subscription
44 | FeatureGroupsFile string `json:"feature_groups_file" envconfig:"feature_groups_file" default:"feature_groups.development.json"`
45 | FeatureGroups []FeatureGroup `json:"-" envconfig:"-"`
46 | PlansFile string `json:"plans_file" envconfig:"plans_file" default:"plans.development.json"`
47 | Plans []Plan `json:"-" envconfig:"-"`
48 | StripePublishableKey string `json:"stripe_publishable_key" envconfig:"stripe_publishable_key"`
49 | StripeSecretKey string `json:"stripe_secret_key" envconfig:"stripe_secret_key"`
50 | StripeWebhookSecret string `json:"stripe_webhook_secret" envconfig:"stripe_webhook_secret"`
51 | }
52 |
53 | type FeatureGroup struct {
54 | Name string `json:"name"`
55 | Features []Feature `json:"features"`
56 | }
57 |
58 | type Feature struct {
59 | ID string `json:"id"`
60 | Title string `json:"title"`
61 | ValueType string `json:"value_type"`
62 | }
63 |
64 | type Plan struct {
65 | PriceID string `json:"price_id"`
66 | Name string `json:"name"`
67 | Price string `json:"price"`
68 | Current bool `json:"-"`
69 | Details []string `json:"details"`
70 | Features map[string]interface{} `json:"features"`
71 |
72 | StripeKey string `json:"-"`
73 | }
74 |
75 | func LoadConfig(configFile string, envPrefix string) (Config, error) {
76 | var config Config
77 | if err := loadEnvironment(configFile); err != nil {
78 | return config, err
79 | }
80 |
81 | if err := envconfig.Process(envPrefix, &config); err != nil {
82 | return config, err
83 | }
84 |
85 | plans, err := loadPlans(config.PlansFile)
86 | if err == nil {
87 | for i := range plans {
88 | plans[i].StripeKey = config.StripePublishableKey
89 | }
90 | config.Plans = plans
91 | } else {
92 | fmt.Printf("err loading plan file %v, err %v \n", config.PlansFile, err)
93 | }
94 |
95 | featureGroups, err := loadFeatureGroups(config.FeatureGroupsFile)
96 | if err == nil {
97 | config.FeatureGroups = featureGroups
98 | } else {
99 | fmt.Printf("err loading feature groups file %v, err %v \n", config.FeatureGroupsFile, err)
100 | }
101 |
102 | return config, nil
103 | }
104 |
105 | func loadPlans(file string) ([]Plan, error) {
106 | if file == "" {
107 | return []Plan{}, nil
108 | }
109 |
110 | var data []byte
111 | var err error
112 |
113 | data, err = base64.StdEncoding.DecodeString(file) // check if string is base64 data
114 | if err != nil {
115 | data, err = ioutil.ReadFile(file) // or is a file path
116 | if err != nil {
117 | return nil, err
118 | }
119 | }
120 |
121 | var plans []Plan
122 | err = json.Unmarshal(data, &plans)
123 | if err != nil {
124 | return nil, err
125 | }
126 |
127 | return plans, nil
128 | }
129 |
130 | func loadFeatureGroups(file string) ([]FeatureGroup, error) {
131 | if file == "" {
132 | return []FeatureGroup{}, nil
133 | }
134 |
135 | var data []byte
136 | var err error
137 |
138 | data, err = base64.StdEncoding.DecodeString(file) // check if string is base64 data
139 | if err != nil {
140 | data, err = ioutil.ReadFile(file) // or is a file path
141 | if err != nil {
142 | return nil, err
143 | }
144 | }
145 |
146 | var featureGroups []FeatureGroup
147 | err = json.Unmarshal(data, &featureGroups)
148 | if err != nil {
149 | return nil, err
150 | }
151 |
152 | return featureGroups, nil
153 | }
154 |
155 | func loadEnvironment(filename string) error {
156 | var err error
157 | if filename != "" {
158 | err = godotenv.Load(filename)
159 | } else {
160 | err = godotenv.Load()
161 | // handle if .env file does not exist, this is OK
162 | if os.IsNotExist(err) {
163 | return nil
164 | }
165 | }
166 | return err
167 | }
168 |
169 | func stringFromMap(m map[string]interface{}, k string) *string {
170 | v, ok := m[k]
171 | if !ok {
172 | return nil
173 | }
174 | val, ok := v.(string)
175 | if !ok {
176 | return nil
177 | }
178 | return &val
179 | }
180 |
181 | func int64FromMap(m map[string]interface{}, k string) *int64 {
182 | v, ok := m[k]
183 | if !ok {
184 | return nil
185 | }
186 | val, ok := v.(int64)
187 | if !ok {
188 | return nil
189 | }
190 | return &val
191 | }
192 |
193 | func float64FromMap(m map[string]interface{}, k string) *float64 {
194 | v, ok := m[k]
195 | if !ok {
196 | return nil
197 | }
198 | val, ok := v.(float64)
199 | if !ok {
200 | return nil
201 | }
202 | return &val
203 | }
204 |
--------------------------------------------------------------------------------
/app/email.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "fmt"
5 | "net/smtp"
6 | "net/textproto"
7 | "strings"
8 | "time"
9 |
10 | "github.com/adnaan/authn"
11 |
12 | "github.com/jordan-wright/email"
13 |
14 | "github.com/matcornic/hermes/v2"
15 | )
16 |
17 | func sendEmailFunc(cfg Config) authn.SendMailFunc {
18 | appName := strings.Title(strings.ToLower(cfg.Name))
19 | h := hermes.Hermes{
20 | Product: hermes.Product{
21 | Name: appName,
22 | Link: cfg.Domain,
23 | //Logo: "https://github.com/matcornic/hermes/blob/master/examples/gopher.png?raw=true",
24 | },
25 | }
26 |
27 | pool := newEmailPool(cfg)
28 | return func(mailType authn.MailType, token, sendTo string, metadata map[string]interface{}) error {
29 | var name string
30 | var ok bool
31 | if metadata["name"] != nil {
32 | name, ok = metadata["name"].(string)
33 | if !ok {
34 | name = ""
35 | }
36 | }
37 |
38 | var emailTmpl hermes.Email
39 | var subject string
40 |
41 | switch mailType {
42 | case authn.Confirmation:
43 | subject = fmt.Sprintf("Welcome to %s!", appName)
44 | emailTmpl = confirmation(appName, name, fmt.Sprintf("%s/confirm/%s", cfg.Domain, token))
45 | case authn.Recovery:
46 | subject = fmt.Sprintf("Reset password on %s", appName)
47 | emailTmpl = recovery(appName, name, fmt.Sprintf("%s/reset/%s", cfg.Domain, token))
48 | case authn.ChangeEmail:
49 | subject = fmt.Sprintf("Change email on %s", appName)
50 | emailTmpl = changeEmail(appName, name, fmt.Sprintf("%s/change/%s", cfg.Domain, token))
51 | case authn.Passwordless:
52 | subject = fmt.Sprintf("Magic link to log into %s", appName)
53 | emailTmpl = magic(appName, name, fmt.Sprintf("%s/magic-login/%s", cfg.Domain, token))
54 | }
55 |
56 | res, err := h.GenerateHTML(emailTmpl)
57 | if err != nil {
58 | return err
59 | }
60 |
61 | e := &email.Email{
62 | To: []string{sendTo},
63 | Subject: subject,
64 | HTML: []byte(res),
65 | Headers: textproto.MIMEHeader{},
66 | From: cfg.SMTPAdminEmail,
67 | }
68 |
69 | return pool.Send(e, 20*time.Second)
70 | }
71 | }
72 |
73 | func confirmation(appName, name, link string) hermes.Email {
74 | return hermes.Email{
75 | Body: hermes.Body{
76 | Name: name,
77 | Intros: []string{
78 | fmt.Sprintf("Welcome to %s! We're very excited to have you on board.", appName),
79 | },
80 | Actions: []hermes.Action{
81 | {
82 | Instructions: fmt.Sprintf("To get started with %s, please click here:", appName),
83 | Button: hermes.Button{
84 | Text: "Confirm your account",
85 | Link: link,
86 | },
87 | },
88 | },
89 | Outros: []string{
90 | "Need help, or have questions? Just reply to this email, we'd love to help.",
91 | },
92 | },
93 | }
94 | }
95 |
96 | func changeEmail(appName, name, link string) hermes.Email {
97 | return hermes.Email{
98 | Body: hermes.Body{
99 | Name: name,
100 | Intros: []string{
101 | fmt.Sprintf("You have received this email because you have requested to change the email linked to your %s account", appName),
102 | },
103 | Actions: []hermes.Action{
104 | {
105 | Instructions: fmt.Sprintf("Click the button below to change the email linked to your %s account", appName),
106 | Button: hermes.Button{
107 | Color: "#DC4D2F",
108 | Text: "Confirm email change",
109 | Link: link,
110 | },
111 | },
112 | },
113 | Outros: []string{
114 | "If you did not request a email change, no further action is required on your part.",
115 | },
116 | Signature: "Thanks",
117 | },
118 | }
119 | }
120 |
121 | func recovery(appName, name, link string) hermes.Email {
122 | return hermes.Email{
123 | Body: hermes.Body{
124 | Name: name,
125 | Intros: []string{
126 | fmt.Sprintf("You have received this email because a password reset request for %s account was received.", appName),
127 | },
128 | Actions: []hermes.Action{
129 | {
130 | Instructions: "Click the button below to reset your password:",
131 | Button: hermes.Button{
132 | Color: "#DC4D2F",
133 | Text: "Reset your password",
134 | Link: link,
135 | },
136 | },
137 | },
138 | Outros: []string{
139 | "If you did not request a password reset, no further action is required on your part.",
140 | },
141 | Signature: "Thanks",
142 | },
143 | }
144 | }
145 |
146 | func magic(appName, name, link string) hermes.Email {
147 | return hermes.Email{
148 | Body: hermes.Body{
149 | Name: name,
150 | Intros: []string{
151 | fmt.Sprintf("You have received this email because a request for a magic login link for your %s account was received.", appName),
152 | },
153 | Actions: []hermes.Action{
154 | {
155 | Instructions: "Click the button below to login:",
156 | Button: hermes.Button{
157 | Text: "Login with magic link",
158 | Link: link,
159 | },
160 | },
161 | },
162 | Outros: []string{
163 | "If you did not request a magic login link, no further action is required on your part.",
164 | },
165 | Signature: "Thanks",
166 | },
167 | }
168 | }
169 |
170 | func newEmailPool(cfg Config) *email.Pool {
171 |
172 | var pool *email.Pool
173 | var err error
174 |
175 | if cfg.SMTPDebug {
176 | pool, err = email.NewPool(
177 | fmt.Sprintf("%s:%d", cfg.SMTPHost, cfg.SMTPPort),
178 | 10, &unencryptedAuth{
179 | smtp.PlainAuth("", cfg.SMTPUser, cfg.SMTPPass, cfg.SMTPHost)},
180 | )
181 |
182 | if err != nil {
183 | panic(err)
184 | }
185 |
186 | return pool
187 | }
188 |
189 | pool, err = email.NewPool(
190 | fmt.Sprintf("%s:%d", cfg.SMTPHost, cfg.SMTPPort),
191 | 10,
192 | smtp.PlainAuth("", cfg.SMTPUser, cfg.SMTPPass, cfg.SMTPHost),
193 | )
194 |
195 | if err != nil {
196 | panic(err)
197 | }
198 |
199 | return pool
200 | }
201 |
202 | type unencryptedAuth struct {
203 | smtp.Auth
204 | }
205 |
206 | // Start starts the auth process for the specified SMTP server.
207 | func (u *unencryptedAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
208 | server.TLS = true
209 | return u.Auth.Start(server)
210 | }
211 |
--------------------------------------------------------------------------------
/app/errors.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/go-chi/render"
8 | )
9 |
10 | // copied from https://github.com/go-chi/chi/blob/master/_examples/rest/main.go#L389
11 | //--
12 | // Error response payloads & renderers
13 | //--
14 |
15 | // ErrResponse renderer type for handling all sorts of errors.
16 | //
17 | // In the best case scenario, the excellent github.com/pkg/errors package
18 | // helps reveal information on the error, setting it on Err, and in the Render()
19 | // method, using it to set the application-specific error code in AppCode.
20 | type ErrResponse struct {
21 | Err error `json:"-"` // low-level runtime error
22 | HTTPStatusCode int `json:"-"` // http response status code
23 |
24 | StatusText string `json:"status"` // user-level status message
25 | AppCode int64 `json:"code,omitempty"` // application-specific error code
26 | ErrorText string `json:"error,omitempty"` // application-level error message, for debugging
27 | }
28 |
29 | func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
30 | render.Status(r, e.HTTPStatusCode)
31 | return nil
32 | }
33 |
34 | func ErrInvalidRequest(err error) render.Renderer {
35 | return &ErrResponse{
36 | Err: err,
37 | HTTPStatusCode: 400,
38 | StatusText: "Invalid request.",
39 | ErrorText: fmt.Sprintf("%v", err),
40 | }
41 | }
42 |
43 | func ErrRender(err error) render.Renderer {
44 | return &ErrResponse{
45 | Err: err,
46 | HTTPStatusCode: 422,
47 | StatusText: "Error rendering response.",
48 | ErrorText: fmt.Sprintf("%v", err),
49 | }
50 | }
51 |
52 | func ErrInternal(err error) render.Renderer {
53 | return &ErrResponse{
54 | Err: err,
55 | HTTPStatusCode: 500,
56 | StatusText: "Internal error.",
57 | ErrorText: fmt.Sprintf("%v", err),
58 | }
59 | }
60 |
61 | func ErrUnauthorized(err error) render.Renderer {
62 | return &ErrResponse{
63 | Err: err,
64 | HTTPStatusCode: 401,
65 | StatusText: "Unauthorized",
66 | ErrorText: fmt.Sprintf("%v", err),
67 | }
68 | }
69 |
70 | var ErrNotFound = &ErrResponse{HTTPStatusCode: 404, StatusText: "Resource not found."}
71 |
--------------------------------------------------------------------------------
/app/gen/models/client.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "log"
9 |
10 | "github.com/adnaan/gomodest-starter/app/gen/models/migrate"
11 |
12 | "github.com/adnaan/gomodest-starter/app/gen/models/task"
13 |
14 | "entgo.io/ent/dialect"
15 | "entgo.io/ent/dialect/sql"
16 | )
17 |
18 | // Client is the client that holds all ent builders.
19 | type Client struct {
20 | config
21 | // Schema is the client for creating, migrating and dropping schema.
22 | Schema *migrate.Schema
23 | // Task is the client for interacting with the Task builders.
24 | Task *TaskClient
25 | }
26 |
27 | // NewClient creates a new client configured with the given options.
28 | func NewClient(opts ...Option) *Client {
29 | cfg := config{log: log.Println, hooks: &hooks{}}
30 | cfg.options(opts...)
31 | client := &Client{config: cfg}
32 | client.init()
33 | return client
34 | }
35 |
36 | func (c *Client) init() {
37 | c.Schema = migrate.NewSchema(c.driver)
38 | c.Task = NewTaskClient(c.config)
39 | }
40 |
41 | // Open opens a database/sql.DB specified by the driver name and
42 | // the data source name, and returns a new client attached to it.
43 | // Optional parameters can be added for configuring the client.
44 | func Open(driverName, dataSourceName string, options ...Option) (*Client, error) {
45 | switch driverName {
46 | case dialect.MySQL, dialect.Postgres, dialect.SQLite:
47 | drv, err := sql.Open(driverName, dataSourceName)
48 | if err != nil {
49 | return nil, err
50 | }
51 | return NewClient(append(options, Driver(drv))...), nil
52 | default:
53 | return nil, fmt.Errorf("unsupported driver: %q", driverName)
54 | }
55 | }
56 |
57 | // Tx returns a new transactional client. The provided context
58 | // is used until the transaction is committed or rolled back.
59 | func (c *Client) Tx(ctx context.Context) (*Tx, error) {
60 | if _, ok := c.driver.(*txDriver); ok {
61 | return nil, fmt.Errorf("models: cannot start a transaction within a transaction")
62 | }
63 | tx, err := newTx(ctx, c.driver)
64 | if err != nil {
65 | return nil, fmt.Errorf("models: starting a transaction: %w", err)
66 | }
67 | cfg := c.config
68 | cfg.driver = tx
69 | return &Tx{
70 | ctx: ctx,
71 | config: cfg,
72 | Task: NewTaskClient(cfg),
73 | }, nil
74 | }
75 |
76 | // BeginTx returns a transactional client with specified options.
77 | func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) {
78 | if _, ok := c.driver.(*txDriver); ok {
79 | return nil, fmt.Errorf("ent: cannot start a transaction within a transaction")
80 | }
81 | tx, err := c.driver.(interface {
82 | BeginTx(context.Context, *sql.TxOptions) (dialect.Tx, error)
83 | }).BeginTx(ctx, opts)
84 | if err != nil {
85 | return nil, fmt.Errorf("ent: starting a transaction: %w", err)
86 | }
87 | cfg := c.config
88 | cfg.driver = &txDriver{tx: tx, drv: c.driver}
89 | return &Tx{
90 | config: cfg,
91 | Task: NewTaskClient(cfg),
92 | }, nil
93 | }
94 |
95 | // Debug returns a new debug-client. It's used to get verbose logging on specific operations.
96 | //
97 | // client.Debug().
98 | // Task.
99 | // Query().
100 | // Count(ctx)
101 | //
102 | func (c *Client) Debug() *Client {
103 | if c.debug {
104 | return c
105 | }
106 | cfg := c.config
107 | cfg.driver = dialect.Debug(c.driver, c.log)
108 | client := &Client{config: cfg}
109 | client.init()
110 | return client
111 | }
112 |
113 | // Close closes the database connection and prevents new queries from starting.
114 | func (c *Client) Close() error {
115 | return c.driver.Close()
116 | }
117 |
118 | // Use adds the mutation hooks to all the entity clients.
119 | // In order to add hooks to a specific client, call: `client.Node.Use(...)`.
120 | func (c *Client) Use(hooks ...Hook) {
121 | c.Task.Use(hooks...)
122 | }
123 |
124 | // TaskClient is a client for the Task schema.
125 | type TaskClient struct {
126 | config
127 | }
128 |
129 | // NewTaskClient returns a client for the Task from the given config.
130 | func NewTaskClient(c config) *TaskClient {
131 | return &TaskClient{config: c}
132 | }
133 |
134 | // Use adds a list of mutation hooks to the hooks stack.
135 | // A call to `Use(f, g, h)` equals to `task.Hooks(f(g(h())))`.
136 | func (c *TaskClient) Use(hooks ...Hook) {
137 | c.hooks.Task = append(c.hooks.Task, hooks...)
138 | }
139 |
140 | // Create returns a create builder for Task.
141 | func (c *TaskClient) Create() *TaskCreate {
142 | mutation := newTaskMutation(c.config, OpCreate)
143 | return &TaskCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
144 | }
145 |
146 | // CreateBulk returns a builder for creating a bulk of Task entities.
147 | func (c *TaskClient) CreateBulk(builders ...*TaskCreate) *TaskCreateBulk {
148 | return &TaskCreateBulk{config: c.config, builders: builders}
149 | }
150 |
151 | // Update returns an update builder for Task.
152 | func (c *TaskClient) Update() *TaskUpdate {
153 | mutation := newTaskMutation(c.config, OpUpdate)
154 | return &TaskUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
155 | }
156 |
157 | // UpdateOne returns an update builder for the given entity.
158 | func (c *TaskClient) UpdateOne(t *Task) *TaskUpdateOne {
159 | mutation := newTaskMutation(c.config, OpUpdateOne, withTask(t))
160 | return &TaskUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
161 | }
162 |
163 | // UpdateOneID returns an update builder for the given id.
164 | func (c *TaskClient) UpdateOneID(id string) *TaskUpdateOne {
165 | mutation := newTaskMutation(c.config, OpUpdateOne, withTaskID(id))
166 | return &TaskUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
167 | }
168 |
169 | // Delete returns a delete builder for Task.
170 | func (c *TaskClient) Delete() *TaskDelete {
171 | mutation := newTaskMutation(c.config, OpDelete)
172 | return &TaskDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
173 | }
174 |
175 | // DeleteOne returns a delete builder for the given entity.
176 | func (c *TaskClient) DeleteOne(t *Task) *TaskDeleteOne {
177 | return c.DeleteOneID(t.ID)
178 | }
179 |
180 | // DeleteOneID returns a delete builder for the given id.
181 | func (c *TaskClient) DeleteOneID(id string) *TaskDeleteOne {
182 | builder := c.Delete().Where(task.ID(id))
183 | builder.mutation.id = &id
184 | builder.mutation.op = OpDeleteOne
185 | return &TaskDeleteOne{builder}
186 | }
187 |
188 | // Query returns a query builder for Task.
189 | func (c *TaskClient) Query() *TaskQuery {
190 | return &TaskQuery{config: c.config}
191 | }
192 |
193 | // Get returns a Task entity by its id.
194 | func (c *TaskClient) Get(ctx context.Context, id string) (*Task, error) {
195 | return c.Query().Where(task.ID(id)).Only(ctx)
196 | }
197 |
198 | // GetX is like Get, but panics if an error occurs.
199 | func (c *TaskClient) GetX(ctx context.Context, id string) *Task {
200 | obj, err := c.Get(ctx, id)
201 | if err != nil {
202 | panic(err)
203 | }
204 | return obj
205 | }
206 |
207 | // Hooks returns the client hooks.
208 | func (c *TaskClient) Hooks() []Hook {
209 | return c.hooks.Task
210 | }
211 |
--------------------------------------------------------------------------------
/app/gen/models/config.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "entgo.io/ent"
7 | "entgo.io/ent/dialect"
8 | )
9 |
10 | // Option function to configure the client.
11 | type Option func(*config)
12 |
13 | // Config is the configuration for the client and its builder.
14 | type config struct {
15 | // driver used for executing database requests.
16 | driver dialect.Driver
17 | // debug enable a debug logging.
18 | debug bool
19 | // log used for logging on debug mode.
20 | log func(...interface{})
21 | // hooks to execute on mutations.
22 | hooks *hooks
23 | }
24 |
25 | // hooks per client, for fast access.
26 | type hooks struct {
27 | Task []ent.Hook
28 | }
29 |
30 | // Options applies the options on the config object.
31 | func (c *config) options(opts ...Option) {
32 | for _, opt := range opts {
33 | opt(c)
34 | }
35 | if c.debug {
36 | c.driver = dialect.Debug(c.driver, c.log)
37 | }
38 | }
39 |
40 | // Debug enables debug logging on the ent.Driver.
41 | func Debug() Option {
42 | return func(c *config) {
43 | c.debug = true
44 | }
45 | }
46 |
47 | // Log sets the logging function for debug mode.
48 | func Log(fn func(...interface{})) Option {
49 | return func(c *config) {
50 | c.log = fn
51 | }
52 | }
53 |
54 | // Driver configures the client driver.
55 | func Driver(driver dialect.Driver) Option {
56 | return func(c *config) {
57 | c.driver = driver
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/gen/models/context.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "context"
7 | )
8 |
9 | type clientCtxKey struct{}
10 |
11 | // FromContext returns a Client stored inside a context, or nil if there isn't one.
12 | func FromContext(ctx context.Context) *Client {
13 | c, _ := ctx.Value(clientCtxKey{}).(*Client)
14 | return c
15 | }
16 |
17 | // NewContext returns a new context with the given Client attached.
18 | func NewContext(parent context.Context, c *Client) context.Context {
19 | return context.WithValue(parent, clientCtxKey{}, c)
20 | }
21 |
22 | type txCtxKey struct{}
23 |
24 | // TxFromContext returns a Tx stored inside a context, or nil if there isn't one.
25 | func TxFromContext(ctx context.Context) *Tx {
26 | tx, _ := ctx.Value(txCtxKey{}).(*Tx)
27 | return tx
28 | }
29 |
30 | // NewTxContext returns a new context with the given Tx attached.
31 | func NewTxContext(parent context.Context, tx *Tx) context.Context {
32 | return context.WithValue(parent, txCtxKey{}, tx)
33 | }
34 |
--------------------------------------------------------------------------------
/app/gen/models/ent.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "errors"
7 | "fmt"
8 |
9 | "entgo.io/ent"
10 | "entgo.io/ent/dialect"
11 | "entgo.io/ent/dialect/sql"
12 | "entgo.io/ent/dialect/sql/sqlgraph"
13 | )
14 |
15 | // ent aliases to avoid import conflicts in user's code.
16 | type (
17 | Op = ent.Op
18 | Hook = ent.Hook
19 | Value = ent.Value
20 | Query = ent.Query
21 | Policy = ent.Policy
22 | Mutator = ent.Mutator
23 | Mutation = ent.Mutation
24 | MutateFunc = ent.MutateFunc
25 | )
26 |
27 | // OrderFunc applies an ordering on the sql selector.
28 | type OrderFunc func(*sql.Selector, func(string) bool)
29 |
30 | // Asc applies the given fields in ASC order.
31 | func Asc(fields ...string) OrderFunc {
32 | return func(s *sql.Selector, check func(string) bool) {
33 | for _, f := range fields {
34 | if check(f) {
35 | s.OrderBy(sql.Asc(f))
36 | } else {
37 | s.AddError(&ValidationError{Name: f, err: fmt.Errorf("invalid field %q for ordering", f)})
38 | }
39 | }
40 | }
41 | }
42 |
43 | // Desc applies the given fields in DESC order.
44 | func Desc(fields ...string) OrderFunc {
45 | return func(s *sql.Selector, check func(string) bool) {
46 | for _, f := range fields {
47 | if check(f) {
48 | s.OrderBy(sql.Desc(f))
49 | } else {
50 | s.AddError(&ValidationError{Name: f, err: fmt.Errorf("invalid field %q for ordering", f)})
51 | }
52 | }
53 | }
54 | }
55 |
56 | // AggregateFunc applies an aggregation step on the group-by traversal/selector.
57 | type AggregateFunc func(*sql.Selector, func(string) bool) string
58 |
59 | // As is a pseudo aggregation function for renaming another other functions with custom names. For example:
60 | //
61 | // GroupBy(field1, field2).
62 | // Aggregate(models.As(models.Sum(field1), "sum_field1"), (models.As(models.Sum(field2), "sum_field2")).
63 | // Scan(ctx, &v)
64 | //
65 | func As(fn AggregateFunc, end string) AggregateFunc {
66 | return func(s *sql.Selector, check func(string) bool) string {
67 | return sql.As(fn(s, check), end)
68 | }
69 | }
70 |
71 | // Count applies the "count" aggregation function on each group.
72 | func Count() AggregateFunc {
73 | return func(s *sql.Selector, _ func(string) bool) string {
74 | return sql.Count("*")
75 | }
76 | }
77 |
78 | // Max applies the "max" aggregation function on the given field of each group.
79 | func Max(field string) AggregateFunc {
80 | return func(s *sql.Selector, check func(string) bool) string {
81 | if !check(field) {
82 | s.AddError(&ValidationError{Name: field, err: fmt.Errorf("invalid field %q for grouping", field)})
83 | return ""
84 | }
85 | return sql.Max(s.C(field))
86 | }
87 | }
88 |
89 | // Mean applies the "mean" aggregation function on the given field of each group.
90 | func Mean(field string) AggregateFunc {
91 | return func(s *sql.Selector, check func(string) bool) string {
92 | if !check(field) {
93 | s.AddError(&ValidationError{Name: field, err: fmt.Errorf("invalid field %q for grouping", field)})
94 | return ""
95 | }
96 | return sql.Avg(s.C(field))
97 | }
98 | }
99 |
100 | // Min applies the "min" aggregation function on the given field of each group.
101 | func Min(field string) AggregateFunc {
102 | return func(s *sql.Selector, check func(string) bool) string {
103 | if !check(field) {
104 | s.AddError(&ValidationError{Name: field, err: fmt.Errorf("invalid field %q for grouping", field)})
105 | return ""
106 | }
107 | return sql.Min(s.C(field))
108 | }
109 | }
110 |
111 | // Sum applies the "sum" aggregation function on the given field of each group.
112 | func Sum(field string) AggregateFunc {
113 | return func(s *sql.Selector, check func(string) bool) string {
114 | if !check(field) {
115 | s.AddError(&ValidationError{Name: field, err: fmt.Errorf("invalid field %q for grouping", field)})
116 | return ""
117 | }
118 | return sql.Sum(s.C(field))
119 | }
120 | }
121 |
122 | // ValidationError returns when validating a field fails.
123 | type ValidationError struct {
124 | Name string // Field or edge name.
125 | err error
126 | }
127 |
128 | // Error implements the error interface.
129 | func (e *ValidationError) Error() string {
130 | return e.err.Error()
131 | }
132 |
133 | // Unwrap implements the errors.Wrapper interface.
134 | func (e *ValidationError) Unwrap() error {
135 | return e.err
136 | }
137 |
138 | // IsValidationError returns a boolean indicating whether the error is a validaton error.
139 | func IsValidationError(err error) bool {
140 | if err == nil {
141 | return false
142 | }
143 | var e *ValidationError
144 | return errors.As(err, &e)
145 | }
146 |
147 | // NotFoundError returns when trying to fetch a specific entity and it was not found in the database.
148 | type NotFoundError struct {
149 | label string
150 | }
151 |
152 | // Error implements the error interface.
153 | func (e *NotFoundError) Error() string {
154 | return "models: " + e.label + " not found"
155 | }
156 |
157 | // IsNotFound returns a boolean indicating whether the error is a not found error.
158 | func IsNotFound(err error) bool {
159 | if err == nil {
160 | return false
161 | }
162 | var e *NotFoundError
163 | return errors.As(err, &e)
164 | }
165 |
166 | // MaskNotFound masks not found error.
167 | func MaskNotFound(err error) error {
168 | if IsNotFound(err) {
169 | return nil
170 | }
171 | return err
172 | }
173 |
174 | // NotSingularError returns when trying to fetch a singular entity and more then one was found in the database.
175 | type NotSingularError struct {
176 | label string
177 | }
178 |
179 | // Error implements the error interface.
180 | func (e *NotSingularError) Error() string {
181 | return "models: " + e.label + " not singular"
182 | }
183 |
184 | // IsNotSingular returns a boolean indicating whether the error is a not singular error.
185 | func IsNotSingular(err error) bool {
186 | if err == nil {
187 | return false
188 | }
189 | var e *NotSingularError
190 | return errors.As(err, &e)
191 | }
192 |
193 | // NotLoadedError returns when trying to get a node that was not loaded by the query.
194 | type NotLoadedError struct {
195 | edge string
196 | }
197 |
198 | // Error implements the error interface.
199 | func (e *NotLoadedError) Error() string {
200 | return "models: " + e.edge + " edge was not loaded"
201 | }
202 |
203 | // IsNotLoaded returns a boolean indicating whether the error is a not loaded error.
204 | func IsNotLoaded(err error) bool {
205 | if err == nil {
206 | return false
207 | }
208 | var e *NotLoadedError
209 | return errors.As(err, &e)
210 | }
211 |
212 | // ConstraintError returns when trying to create/update one or more entities and
213 | // one or more of their constraints failed. For example, violation of edge or
214 | // field uniqueness.
215 | type ConstraintError struct {
216 | msg string
217 | wrap error
218 | }
219 |
220 | // Error implements the error interface.
221 | func (e ConstraintError) Error() string {
222 | return "models: constraint failed: " + e.msg
223 | }
224 |
225 | // Unwrap implements the errors.Wrapper interface.
226 | func (e *ConstraintError) Unwrap() error {
227 | return e.wrap
228 | }
229 |
230 | // IsConstraintError returns a boolean indicating whether the error is a constraint failure.
231 | func IsConstraintError(err error) bool {
232 | if err == nil {
233 | return false
234 | }
235 | var e *ConstraintError
236 | return errors.As(err, &e)
237 | }
238 |
239 | func isSQLConstraintError(err error) (*ConstraintError, bool) {
240 | if sqlgraph.IsConstraintError(err) {
241 | return &ConstraintError{err.Error(), err}, true
242 | }
243 | return nil, false
244 | }
245 |
246 | // rollback calls tx.Rollback and wraps the given error with the rollback error if present.
247 | func rollback(tx dialect.Tx, err error) error {
248 | if rerr := tx.Rollback(); rerr != nil {
249 | err = fmt.Errorf("%w: %v", err, rerr)
250 | }
251 | if err, ok := isSQLConstraintError(err); ok {
252 | return err
253 | }
254 | return err
255 | }
256 |
--------------------------------------------------------------------------------
/app/gen/models/enttest/enttest.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package enttest
4 |
5 | import (
6 | "context"
7 |
8 | "github.com/adnaan/gomodest-starter/app/gen/models"
9 | // required by schema hooks.
10 | _ "github.com/adnaan/gomodest-starter/app/gen/models/runtime"
11 |
12 | "entgo.io/ent/dialect/sql/schema"
13 | )
14 |
15 | type (
16 | // TestingT is the interface that is shared between
17 | // testing.T and testing.B and used by enttest.
18 | TestingT interface {
19 | FailNow()
20 | Error(...interface{})
21 | }
22 |
23 | // Option configures client creation.
24 | Option func(*options)
25 |
26 | options struct {
27 | opts []models.Option
28 | migrateOpts []schema.MigrateOption
29 | }
30 | )
31 |
32 | // WithOptions forwards options to client creation.
33 | func WithOptions(opts ...models.Option) Option {
34 | return func(o *options) {
35 | o.opts = append(o.opts, opts...)
36 | }
37 | }
38 |
39 | // WithMigrateOptions forwards options to auto migration.
40 | func WithMigrateOptions(opts ...schema.MigrateOption) Option {
41 | return func(o *options) {
42 | o.migrateOpts = append(o.migrateOpts, opts...)
43 | }
44 | }
45 |
46 | func newOptions(opts []Option) *options {
47 | o := &options{}
48 | for _, opt := range opts {
49 | opt(o)
50 | }
51 | return o
52 | }
53 |
54 | // Open calls models.Open and auto-run migration.
55 | func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *models.Client {
56 | o := newOptions(opts)
57 | c, err := models.Open(driverName, dataSourceName, o.opts...)
58 | if err != nil {
59 | t.Error(err)
60 | t.FailNow()
61 | }
62 | if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
63 | t.Error(err)
64 | t.FailNow()
65 | }
66 | return c
67 | }
68 |
69 | // NewClient calls models.NewClient and auto-run migration.
70 | func NewClient(t TestingT, opts ...Option) *models.Client {
71 | o := newOptions(opts)
72 | c := models.NewClient(o.opts...)
73 | if err := c.Schema.Create(context.Background(), o.migrateOpts...); err != nil {
74 | t.Error(err)
75 | t.FailNow()
76 | }
77 | return c
78 | }
79 |
--------------------------------------------------------------------------------
/app/gen/models/hook/hook.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package hook
4 |
5 | import (
6 | "context"
7 | "fmt"
8 |
9 | "github.com/adnaan/gomodest-starter/app/gen/models"
10 | )
11 |
12 | // The TaskFunc type is an adapter to allow the use of ordinary
13 | // function as Task mutator.
14 | type TaskFunc func(context.Context, *models.TaskMutation) (models.Value, error)
15 |
16 | // Mutate calls f(ctx, m).
17 | func (f TaskFunc) Mutate(ctx context.Context, m models.Mutation) (models.Value, error) {
18 | mv, ok := m.(*models.TaskMutation)
19 | if !ok {
20 | return nil, fmt.Errorf("unexpected mutation type %T. expect *models.TaskMutation", m)
21 | }
22 | return f(ctx, mv)
23 | }
24 |
25 | // Condition is a hook condition function.
26 | type Condition func(context.Context, models.Mutation) bool
27 |
28 | // And groups conditions with the AND operator.
29 | func And(first, second Condition, rest ...Condition) Condition {
30 | return func(ctx context.Context, m models.Mutation) bool {
31 | if !first(ctx, m) || !second(ctx, m) {
32 | return false
33 | }
34 | for _, cond := range rest {
35 | if !cond(ctx, m) {
36 | return false
37 | }
38 | }
39 | return true
40 | }
41 | }
42 |
43 | // Or groups conditions with the OR operator.
44 | func Or(first, second Condition, rest ...Condition) Condition {
45 | return func(ctx context.Context, m models.Mutation) bool {
46 | if first(ctx, m) || second(ctx, m) {
47 | return true
48 | }
49 | for _, cond := range rest {
50 | if cond(ctx, m) {
51 | return true
52 | }
53 | }
54 | return false
55 | }
56 | }
57 |
58 | // Not negates a given condition.
59 | func Not(cond Condition) Condition {
60 | return func(ctx context.Context, m models.Mutation) bool {
61 | return !cond(ctx, m)
62 | }
63 | }
64 |
65 | // HasOp is a condition testing mutation operation.
66 | func HasOp(op models.Op) Condition {
67 | return func(_ context.Context, m models.Mutation) bool {
68 | return m.Op().Is(op)
69 | }
70 | }
71 |
72 | // HasAddedFields is a condition validating `.AddedField` on fields.
73 | func HasAddedFields(field string, fields ...string) Condition {
74 | return func(_ context.Context, m models.Mutation) bool {
75 | if _, exists := m.AddedField(field); !exists {
76 | return false
77 | }
78 | for _, field := range fields {
79 | if _, exists := m.AddedField(field); !exists {
80 | return false
81 | }
82 | }
83 | return true
84 | }
85 | }
86 |
87 | // HasClearedFields is a condition validating `.FieldCleared` on fields.
88 | func HasClearedFields(field string, fields ...string) Condition {
89 | return func(_ context.Context, m models.Mutation) bool {
90 | if exists := m.FieldCleared(field); !exists {
91 | return false
92 | }
93 | for _, field := range fields {
94 | if exists := m.FieldCleared(field); !exists {
95 | return false
96 | }
97 | }
98 | return true
99 | }
100 | }
101 |
102 | // HasFields is a condition validating `.Field` on fields.
103 | func HasFields(field string, fields ...string) Condition {
104 | return func(_ context.Context, m models.Mutation) bool {
105 | if _, exists := m.Field(field); !exists {
106 | return false
107 | }
108 | for _, field := range fields {
109 | if _, exists := m.Field(field); !exists {
110 | return false
111 | }
112 | }
113 | return true
114 | }
115 | }
116 |
117 | // If executes the given hook under condition.
118 | //
119 | // hook.If(ComputeAverage, And(HasFields(...), HasAddedFields(...)))
120 | //
121 | func If(hk models.Hook, cond Condition) models.Hook {
122 | return func(next models.Mutator) models.Mutator {
123 | return models.MutateFunc(func(ctx context.Context, m models.Mutation) (models.Value, error) {
124 | if cond(ctx, m) {
125 | return hk(next).Mutate(ctx, m)
126 | }
127 | return next.Mutate(ctx, m)
128 | })
129 | }
130 | }
131 |
132 | // On executes the given hook only for the given operation.
133 | //
134 | // hook.On(Log, models.Delete|models.Create)
135 | //
136 | func On(hk models.Hook, op models.Op) models.Hook {
137 | return If(hk, HasOp(op))
138 | }
139 |
140 | // Unless skips the given hook only for the given operation.
141 | //
142 | // hook.Unless(Log, models.Update|models.UpdateOne)
143 | //
144 | func Unless(hk models.Hook, op models.Op) models.Hook {
145 | return If(hk, Not(HasOp(op)))
146 | }
147 |
148 | // FixedError is a hook returning a fixed error.
149 | func FixedError(err error) models.Hook {
150 | return func(models.Mutator) models.Mutator {
151 | return models.MutateFunc(func(context.Context, models.Mutation) (models.Value, error) {
152 | return nil, err
153 | })
154 | }
155 | }
156 |
157 | // Reject returns a hook that rejects all operations that match op.
158 | //
159 | // func (T) Hooks() []models.Hook {
160 | // return []models.Hook{
161 | // Reject(models.Delete|models.Update),
162 | // }
163 | // }
164 | //
165 | func Reject(op models.Op) models.Hook {
166 | hk := FixedError(fmt.Errorf("%s operation is not allowed", op))
167 | return On(hk, op)
168 | }
169 |
170 | // Chain acts as a list of hooks and is effectively immutable.
171 | // Once created, it will always hold the same set of hooks in the same order.
172 | type Chain struct {
173 | hooks []models.Hook
174 | }
175 |
176 | // NewChain creates a new chain of hooks.
177 | func NewChain(hooks ...models.Hook) Chain {
178 | return Chain{append([]models.Hook(nil), hooks...)}
179 | }
180 |
181 | // Hook chains the list of hooks and returns the final hook.
182 | func (c Chain) Hook() models.Hook {
183 | return func(mutator models.Mutator) models.Mutator {
184 | for i := len(c.hooks) - 1; i >= 0; i-- {
185 | mutator = c.hooks[i](mutator)
186 | }
187 | return mutator
188 | }
189 | }
190 |
191 | // Append extends a chain, adding the specified hook
192 | // as the last ones in the mutation flow.
193 | func (c Chain) Append(hooks ...models.Hook) Chain {
194 | newHooks := make([]models.Hook, 0, len(c.hooks)+len(hooks))
195 | newHooks = append(newHooks, c.hooks...)
196 | newHooks = append(newHooks, hooks...)
197 | return Chain{newHooks}
198 | }
199 |
200 | // Extend extends a chain, adding the specified chain
201 | // as the last ones in the mutation flow.
202 | func (c Chain) Extend(chain Chain) Chain {
203 | return c.Append(chain.hooks...)
204 | }
205 |
--------------------------------------------------------------------------------
/app/gen/models/migrate/migrate.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package migrate
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "io"
9 |
10 | "entgo.io/ent/dialect"
11 | "entgo.io/ent/dialect/sql/schema"
12 | )
13 |
14 | var (
15 | // WithGlobalUniqueID sets the universal ids options to the migration.
16 | // If this option is enabled, ent migration will allocate a 1<<32 range
17 | // for the ids of each entity (table).
18 | // Note that this option cannot be applied on tables that already exist.
19 | WithGlobalUniqueID = schema.WithGlobalUniqueID
20 | // WithDropColumn sets the drop column option to the migration.
21 | // If this option is enabled, ent migration will drop old columns
22 | // that were used for both fields and edges. This defaults to false.
23 | WithDropColumn = schema.WithDropColumn
24 | // WithDropIndex sets the drop index option to the migration.
25 | // If this option is enabled, ent migration will drop old indexes
26 | // that were defined in the schema. This defaults to false.
27 | // Note that unique constraints are defined using `UNIQUE INDEX`,
28 | // and therefore, it's recommended to enable this option to get more
29 | // flexibility in the schema changes.
30 | WithDropIndex = schema.WithDropIndex
31 | // WithFixture sets the foreign-key renaming option to the migration when upgrading
32 | // ent from v0.1.0 (issue-#285). Defaults to false.
33 | WithFixture = schema.WithFixture
34 | // WithForeignKeys enables creating foreign-key in schema DDL. This defaults to true.
35 | WithForeignKeys = schema.WithForeignKeys
36 | )
37 |
38 | // Schema is the API for creating, migrating and dropping a schema.
39 | type Schema struct {
40 | drv dialect.Driver
41 | universalID bool
42 | }
43 |
44 | // NewSchema creates a new schema client.
45 | func NewSchema(drv dialect.Driver) *Schema { return &Schema{drv: drv} }
46 |
47 | // Create creates all schema resources.
48 | func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error {
49 | migrate, err := schema.NewMigrate(s.drv, opts...)
50 | if err != nil {
51 | return fmt.Errorf("ent/migrate: %w", err)
52 | }
53 | return migrate.Create(ctx, Tables...)
54 | }
55 |
56 | // WriteTo writes the schema changes to w instead of running them against the database.
57 | //
58 | // if err := client.Schema.WriteTo(context.Background(), os.Stdout); err != nil {
59 | // log.Fatal(err)
60 | // }
61 | //
62 | func (s *Schema) WriteTo(ctx context.Context, w io.Writer, opts ...schema.MigrateOption) error {
63 | drv := &schema.WriteDriver{
64 | Writer: w,
65 | Driver: s.drv,
66 | }
67 | migrate, err := schema.NewMigrate(drv, opts...)
68 | if err != nil {
69 | return fmt.Errorf("ent/migrate: %w", err)
70 | }
71 | return migrate.Create(ctx, Tables...)
72 | }
73 |
--------------------------------------------------------------------------------
/app/gen/models/migrate/schema.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package migrate
4 |
5 | import (
6 | "entgo.io/ent/dialect/entsql"
7 | "entgo.io/ent/dialect/sql/schema"
8 | "entgo.io/ent/schema/field"
9 | )
10 |
11 | var (
12 | // TasksColumns holds the columns for the "tasks" table.
13 | TasksColumns = []*schema.Column{
14 | {Name: "id", Type: field.TypeString},
15 | {Name: "owner", Type: field.TypeString},
16 | {Name: "text", Type: field.TypeString, Size: 2147483647},
17 | {Name: "status", Type: field.TypeEnum, Nullable: true, Enums: []string{"todo", "inprogress", "done"}, Default: "todo"},
18 | {Name: "created_at", Type: field.TypeTime},
19 | {Name: "updated_at", Type: field.TypeTime},
20 | }
21 | // TasksTable holds the schema information for the "tasks" table.
22 | TasksTable = &schema.Table{
23 | Name: "tasks",
24 | Columns: TasksColumns,
25 | PrimaryKey: []*schema.Column{TasksColumns[0]},
26 | ForeignKeys: []*schema.ForeignKey{},
27 | }
28 | // Tables holds all the tables in the schema.
29 | Tables = []*schema.Table{
30 | TasksTable,
31 | }
32 | )
33 |
34 | func init() {
35 | TasksTable.Annotation = &entsql.Annotation{
36 | Table: "tasks",
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/gen/models/predicate/predicate.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package predicate
4 |
5 | import (
6 | "entgo.io/ent/dialect/sql"
7 | )
8 |
9 | // Task is the predicate function for task builders.
10 | type Task func(*sql.Selector)
11 |
--------------------------------------------------------------------------------
/app/gen/models/runtime.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "time"
7 |
8 | "github.com/adnaan/gomodest-starter/app/gen/models/task"
9 | "github.com/adnaan/gomodest-starter/app/schema"
10 | )
11 |
12 | // The init function reads all schema descriptors with runtime code
13 | // (default values, validators, hooks and policies) and stitches it
14 | // to their package variables.
15 | func init() {
16 | taskFields := schema.Task{}.Fields()
17 | _ = taskFields
18 | // taskDescCreatedAt is the schema descriptor for created_at field.
19 | taskDescCreatedAt := taskFields[4].Descriptor()
20 | // task.DefaultCreatedAt holds the default value on creation for the created_at field.
21 | task.DefaultCreatedAt = taskDescCreatedAt.Default.(func() time.Time)
22 | // taskDescUpdatedAt is the schema descriptor for updated_at field.
23 | taskDescUpdatedAt := taskFields[5].Descriptor()
24 | // task.DefaultUpdatedAt holds the default value on creation for the updated_at field.
25 | task.DefaultUpdatedAt = taskDescUpdatedAt.Default.(func() time.Time)
26 | // task.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
27 | task.UpdateDefaultUpdatedAt = taskDescUpdatedAt.UpdateDefault.(func() time.Time)
28 | }
29 |
--------------------------------------------------------------------------------
/app/gen/models/runtime/runtime.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package runtime
4 |
5 | // The schema-stitching logic is generated in github.com/adnaan/gomodest-starter/app/gen/models/runtime.go
6 |
7 | const (
8 | Version = "(devel)" // Version of ent codegen.
9 | )
10 |
--------------------------------------------------------------------------------
/app/gen/models/task.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "fmt"
7 | "strings"
8 | "time"
9 |
10 | "entgo.io/ent/dialect/sql"
11 | "github.com/adnaan/gomodest-starter/app/gen/models/task"
12 | )
13 |
14 | // Task is the model entity for the Task schema.
15 | type Task struct {
16 | config `json:"-"`
17 | // ID of the ent.
18 | ID string `json:"id,omitempty"`
19 | // Owner holds the value of the "owner" field.
20 | Owner string `json:"owner,omitempty"`
21 | // Text holds the value of the "text" field.
22 | Text string `json:"text,omitempty"`
23 | // Status holds the value of the "status" field.
24 | Status task.Status `json:"status,omitempty"`
25 | // CreatedAt holds the value of the "created_at" field.
26 | CreatedAt time.Time `json:"created_at,omitempty"`
27 | // UpdatedAt holds the value of the "updated_at" field.
28 | UpdatedAt time.Time `json:"updated_at,omitempty"`
29 | }
30 |
31 | // scanValues returns the types for scanning values from sql.Rows.
32 | func (*Task) scanValues(columns []string) ([]interface{}, error) {
33 | values := make([]interface{}, len(columns))
34 | for i := range columns {
35 | switch columns[i] {
36 | case task.FieldID, task.FieldOwner, task.FieldText, task.FieldStatus:
37 | values[i] = &sql.NullString{}
38 | case task.FieldCreatedAt, task.FieldUpdatedAt:
39 | values[i] = &sql.NullTime{}
40 | default:
41 | return nil, fmt.Errorf("unexpected column %q for type Task", columns[i])
42 | }
43 | }
44 | return values, nil
45 | }
46 |
47 | // assignValues assigns the values that were returned from sql.Rows (after scanning)
48 | // to the Task fields.
49 | func (t *Task) assignValues(columns []string, values []interface{}) error {
50 | if m, n := len(values), len(columns); m < n {
51 | return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
52 | }
53 | for i := range columns {
54 | switch columns[i] {
55 | case task.FieldID:
56 | if value, ok := values[i].(*sql.NullString); !ok {
57 | return fmt.Errorf("unexpected type %T for field id", values[i])
58 | } else if value.Valid {
59 | t.ID = value.String
60 | }
61 | case task.FieldOwner:
62 | if value, ok := values[i].(*sql.NullString); !ok {
63 | return fmt.Errorf("unexpected type %T for field owner", values[i])
64 | } else if value.Valid {
65 | t.Owner = value.String
66 | }
67 | case task.FieldText:
68 | if value, ok := values[i].(*sql.NullString); !ok {
69 | return fmt.Errorf("unexpected type %T for field text", values[i])
70 | } else if value.Valid {
71 | t.Text = value.String
72 | }
73 | case task.FieldStatus:
74 | if value, ok := values[i].(*sql.NullString); !ok {
75 | return fmt.Errorf("unexpected type %T for field status", values[i])
76 | } else if value.Valid {
77 | t.Status = task.Status(value.String)
78 | }
79 | case task.FieldCreatedAt:
80 | if value, ok := values[i].(*sql.NullTime); !ok {
81 | return fmt.Errorf("unexpected type %T for field created_at", values[i])
82 | } else if value.Valid {
83 | t.CreatedAt = value.Time
84 | }
85 | case task.FieldUpdatedAt:
86 | if value, ok := values[i].(*sql.NullTime); !ok {
87 | return fmt.Errorf("unexpected type %T for field updated_at", values[i])
88 | } else if value.Valid {
89 | t.UpdatedAt = value.Time
90 | }
91 | }
92 | }
93 | return nil
94 | }
95 |
96 | // Update returns a builder for updating this Task.
97 | // Note that you need to call Task.Unwrap() before calling this method if this Task
98 | // was returned from a transaction, and the transaction was committed or rolled back.
99 | func (t *Task) Update() *TaskUpdateOne {
100 | return (&TaskClient{config: t.config}).UpdateOne(t)
101 | }
102 |
103 | // Unwrap unwraps the Task entity that was returned from a transaction after it was closed,
104 | // so that all future queries will be executed through the driver which created the transaction.
105 | func (t *Task) Unwrap() *Task {
106 | tx, ok := t.config.driver.(*txDriver)
107 | if !ok {
108 | panic("models: Task is not a transactional entity")
109 | }
110 | t.config.driver = tx.drv
111 | return t
112 | }
113 |
114 | // String implements the fmt.Stringer.
115 | func (t *Task) String() string {
116 | var builder strings.Builder
117 | builder.WriteString("Task(")
118 | builder.WriteString(fmt.Sprintf("id=%v", t.ID))
119 | builder.WriteString(", owner=")
120 | builder.WriteString(t.Owner)
121 | builder.WriteString(", text=")
122 | builder.WriteString(t.Text)
123 | builder.WriteString(", status=")
124 | builder.WriteString(fmt.Sprintf("%v", t.Status))
125 | builder.WriteString(", created_at=")
126 | builder.WriteString(t.CreatedAt.Format(time.ANSIC))
127 | builder.WriteString(", updated_at=")
128 | builder.WriteString(t.UpdatedAt.Format(time.ANSIC))
129 | builder.WriteByte(')')
130 | return builder.String()
131 | }
132 |
133 | // Tasks is a parsable slice of Task.
134 | type Tasks []*Task
135 |
136 | func (t Tasks) config(cfg config) {
137 | for _i := range t {
138 | t[_i].config = cfg
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/app/gen/models/task/task.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package task
4 |
5 | import (
6 | "fmt"
7 | "time"
8 | )
9 |
10 | const (
11 | // Label holds the string label denoting the task type in the database.
12 | Label = "task"
13 | // FieldID holds the string denoting the id field in the database.
14 | FieldID = "id"
15 | // FieldOwner holds the string denoting the owner field in the database.
16 | FieldOwner = "owner"
17 | // FieldText holds the string denoting the text field in the database.
18 | FieldText = "text"
19 | // FieldStatus holds the string denoting the status field in the database.
20 | FieldStatus = "status"
21 | // FieldCreatedAt holds the string denoting the created_at field in the database.
22 | FieldCreatedAt = "created_at"
23 | // FieldUpdatedAt holds the string denoting the updated_at field in the database.
24 | FieldUpdatedAt = "updated_at"
25 | // Table holds the table name of the task in the database.
26 | Table = "tasks"
27 | )
28 |
29 | // Columns holds all SQL columns for task fields.
30 | var Columns = []string{
31 | FieldID,
32 | FieldOwner,
33 | FieldText,
34 | FieldStatus,
35 | FieldCreatedAt,
36 | FieldUpdatedAt,
37 | }
38 |
39 | // ValidColumn reports if the column name is valid (part of the table columns).
40 | func ValidColumn(column string) bool {
41 | for i := range Columns {
42 | if column == Columns[i] {
43 | return true
44 | }
45 | }
46 | return false
47 | }
48 |
49 | var (
50 | // DefaultCreatedAt holds the default value on creation for the "created_at" field.
51 | DefaultCreatedAt func() time.Time
52 | // DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
53 | DefaultUpdatedAt func() time.Time
54 | // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
55 | UpdateDefaultUpdatedAt func() time.Time
56 | )
57 |
58 | // Status defines the type for the "status" enum field.
59 | type Status string
60 |
61 | // StatusTodo is the default value of the Status enum.
62 | const DefaultStatus = StatusTodo
63 |
64 | // Status values.
65 | const (
66 | StatusTodo Status = "todo"
67 | StatusInprogress Status = "inprogress"
68 | StatusDone Status = "done"
69 | )
70 |
71 | func (s Status) String() string {
72 | return string(s)
73 | }
74 |
75 | // StatusValidator is a validator for the "status" field enum values. It is called by the builders before save.
76 | func StatusValidator(s Status) error {
77 | switch s {
78 | case StatusTodo, StatusInprogress, StatusDone:
79 | return nil
80 | default:
81 | return fmt.Errorf("task: invalid enum value for status field: %q", s)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/gen/models/task_create.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "context"
7 | "errors"
8 | "fmt"
9 | "time"
10 |
11 | "entgo.io/ent/dialect/sql/sqlgraph"
12 | "entgo.io/ent/schema/field"
13 | "github.com/adnaan/gomodest-starter/app/gen/models/task"
14 | )
15 |
16 | // TaskCreate is the builder for creating a Task entity.
17 | type TaskCreate struct {
18 | config
19 | mutation *TaskMutation
20 | hooks []Hook
21 | }
22 |
23 | // SetOwner sets the "owner" field.
24 | func (tc *TaskCreate) SetOwner(s string) *TaskCreate {
25 | tc.mutation.SetOwner(s)
26 | return tc
27 | }
28 |
29 | // SetText sets the "text" field.
30 | func (tc *TaskCreate) SetText(s string) *TaskCreate {
31 | tc.mutation.SetText(s)
32 | return tc
33 | }
34 |
35 | // SetStatus sets the "status" field.
36 | func (tc *TaskCreate) SetStatus(t task.Status) *TaskCreate {
37 | tc.mutation.SetStatus(t)
38 | return tc
39 | }
40 |
41 | // SetNillableStatus sets the "status" field if the given value is not nil.
42 | func (tc *TaskCreate) SetNillableStatus(t *task.Status) *TaskCreate {
43 | if t != nil {
44 | tc.SetStatus(*t)
45 | }
46 | return tc
47 | }
48 |
49 | // SetCreatedAt sets the "created_at" field.
50 | func (tc *TaskCreate) SetCreatedAt(t time.Time) *TaskCreate {
51 | tc.mutation.SetCreatedAt(t)
52 | return tc
53 | }
54 |
55 | // SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
56 | func (tc *TaskCreate) SetNillableCreatedAt(t *time.Time) *TaskCreate {
57 | if t != nil {
58 | tc.SetCreatedAt(*t)
59 | }
60 | return tc
61 | }
62 |
63 | // SetUpdatedAt sets the "updated_at" field.
64 | func (tc *TaskCreate) SetUpdatedAt(t time.Time) *TaskCreate {
65 | tc.mutation.SetUpdatedAt(t)
66 | return tc
67 | }
68 |
69 | // SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil.
70 | func (tc *TaskCreate) SetNillableUpdatedAt(t *time.Time) *TaskCreate {
71 | if t != nil {
72 | tc.SetUpdatedAt(*t)
73 | }
74 | return tc
75 | }
76 |
77 | // SetID sets the "id" field.
78 | func (tc *TaskCreate) SetID(s string) *TaskCreate {
79 | tc.mutation.SetID(s)
80 | return tc
81 | }
82 |
83 | // Mutation returns the TaskMutation object of the builder.
84 | func (tc *TaskCreate) Mutation() *TaskMutation {
85 | return tc.mutation
86 | }
87 |
88 | // Save creates the Task in the database.
89 | func (tc *TaskCreate) Save(ctx context.Context) (*Task, error) {
90 | var (
91 | err error
92 | node *Task
93 | )
94 | tc.defaults()
95 | if len(tc.hooks) == 0 {
96 | if err = tc.check(); err != nil {
97 | return nil, err
98 | }
99 | node, err = tc.sqlSave(ctx)
100 | } else {
101 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
102 | mutation, ok := m.(*TaskMutation)
103 | if !ok {
104 | return nil, fmt.Errorf("unexpected mutation type %T", m)
105 | }
106 | if err = tc.check(); err != nil {
107 | return nil, err
108 | }
109 | tc.mutation = mutation
110 | node, err = tc.sqlSave(ctx)
111 | mutation.done = true
112 | return node, err
113 | })
114 | for i := len(tc.hooks) - 1; i >= 0; i-- {
115 | mut = tc.hooks[i](mut)
116 | }
117 | if _, err := mut.Mutate(ctx, tc.mutation); err != nil {
118 | return nil, err
119 | }
120 | }
121 | return node, err
122 | }
123 |
124 | // SaveX calls Save and panics if Save returns an error.
125 | func (tc *TaskCreate) SaveX(ctx context.Context) *Task {
126 | v, err := tc.Save(ctx)
127 | if err != nil {
128 | panic(err)
129 | }
130 | return v
131 | }
132 |
133 | // defaults sets the default values of the builder before save.
134 | func (tc *TaskCreate) defaults() {
135 | if _, ok := tc.mutation.Status(); !ok {
136 | v := task.DefaultStatus
137 | tc.mutation.SetStatus(v)
138 | }
139 | if _, ok := tc.mutation.CreatedAt(); !ok {
140 | v := task.DefaultCreatedAt()
141 | tc.mutation.SetCreatedAt(v)
142 | }
143 | if _, ok := tc.mutation.UpdatedAt(); !ok {
144 | v := task.DefaultUpdatedAt()
145 | tc.mutation.SetUpdatedAt(v)
146 | }
147 | }
148 |
149 | // check runs all checks and user-defined validators on the builder.
150 | func (tc *TaskCreate) check() error {
151 | if _, ok := tc.mutation.Owner(); !ok {
152 | return &ValidationError{Name: "owner", err: errors.New("models: missing required field \"owner\"")}
153 | }
154 | if _, ok := tc.mutation.Text(); !ok {
155 | return &ValidationError{Name: "text", err: errors.New("models: missing required field \"text\"")}
156 | }
157 | if v, ok := tc.mutation.Status(); ok {
158 | if err := task.StatusValidator(v); err != nil {
159 | return &ValidationError{Name: "status", err: fmt.Errorf("models: validator failed for field \"status\": %w", err)}
160 | }
161 | }
162 | if _, ok := tc.mutation.CreatedAt(); !ok {
163 | return &ValidationError{Name: "created_at", err: errors.New("models: missing required field \"created_at\"")}
164 | }
165 | if _, ok := tc.mutation.UpdatedAt(); !ok {
166 | return &ValidationError{Name: "updated_at", err: errors.New("models: missing required field \"updated_at\"")}
167 | }
168 | return nil
169 | }
170 |
171 | func (tc *TaskCreate) sqlSave(ctx context.Context) (*Task, error) {
172 | _node, _spec := tc.createSpec()
173 | if err := sqlgraph.CreateNode(ctx, tc.driver, _spec); err != nil {
174 | if cerr, ok := isSQLConstraintError(err); ok {
175 | err = cerr
176 | }
177 | return nil, err
178 | }
179 | return _node, nil
180 | }
181 |
182 | func (tc *TaskCreate) createSpec() (*Task, *sqlgraph.CreateSpec) {
183 | var (
184 | _node = &Task{config: tc.config}
185 | _spec = &sqlgraph.CreateSpec{
186 | Table: task.Table,
187 | ID: &sqlgraph.FieldSpec{
188 | Type: field.TypeString,
189 | Column: task.FieldID,
190 | },
191 | }
192 | )
193 | if id, ok := tc.mutation.ID(); ok {
194 | _node.ID = id
195 | _spec.ID.Value = id
196 | }
197 | if value, ok := tc.mutation.Owner(); ok {
198 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
199 | Type: field.TypeString,
200 | Value: value,
201 | Column: task.FieldOwner,
202 | })
203 | _node.Owner = value
204 | }
205 | if value, ok := tc.mutation.Text(); ok {
206 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
207 | Type: field.TypeString,
208 | Value: value,
209 | Column: task.FieldText,
210 | })
211 | _node.Text = value
212 | }
213 | if value, ok := tc.mutation.Status(); ok {
214 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
215 | Type: field.TypeEnum,
216 | Value: value,
217 | Column: task.FieldStatus,
218 | })
219 | _node.Status = value
220 | }
221 | if value, ok := tc.mutation.CreatedAt(); ok {
222 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
223 | Type: field.TypeTime,
224 | Value: value,
225 | Column: task.FieldCreatedAt,
226 | })
227 | _node.CreatedAt = value
228 | }
229 | if value, ok := tc.mutation.UpdatedAt(); ok {
230 | _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{
231 | Type: field.TypeTime,
232 | Value: value,
233 | Column: task.FieldUpdatedAt,
234 | })
235 | _node.UpdatedAt = value
236 | }
237 | return _node, _spec
238 | }
239 |
240 | // TaskCreateBulk is the builder for creating many Task entities in bulk.
241 | type TaskCreateBulk struct {
242 | config
243 | builders []*TaskCreate
244 | }
245 |
246 | // Save creates the Task entities in the database.
247 | func (tcb *TaskCreateBulk) Save(ctx context.Context) ([]*Task, error) {
248 | specs := make([]*sqlgraph.CreateSpec, len(tcb.builders))
249 | nodes := make([]*Task, len(tcb.builders))
250 | mutators := make([]Mutator, len(tcb.builders))
251 | for i := range tcb.builders {
252 | func(i int, root context.Context) {
253 | builder := tcb.builders[i]
254 | builder.defaults()
255 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
256 | mutation, ok := m.(*TaskMutation)
257 | if !ok {
258 | return nil, fmt.Errorf("unexpected mutation type %T", m)
259 | }
260 | if err := builder.check(); err != nil {
261 | return nil, err
262 | }
263 | builder.mutation = mutation
264 | nodes[i], specs[i] = builder.createSpec()
265 | var err error
266 | if i < len(mutators)-1 {
267 | _, err = mutators[i+1].Mutate(root, tcb.builders[i+1].mutation)
268 | } else {
269 | // Invoke the actual operation on the latest mutation in the chain.
270 | if err = sqlgraph.BatchCreate(ctx, tcb.driver, &sqlgraph.BatchCreateSpec{Nodes: specs}); err != nil {
271 | if cerr, ok := isSQLConstraintError(err); ok {
272 | err = cerr
273 | }
274 | }
275 | }
276 | mutation.done = true
277 | if err != nil {
278 | return nil, err
279 | }
280 | return nodes[i], nil
281 | })
282 | for i := len(builder.hooks) - 1; i >= 0; i-- {
283 | mut = builder.hooks[i](mut)
284 | }
285 | mutators[i] = mut
286 | }(i, ctx)
287 | }
288 | if len(mutators) > 0 {
289 | if _, err := mutators[0].Mutate(ctx, tcb.builders[0].mutation); err != nil {
290 | return nil, err
291 | }
292 | }
293 | return nodes, nil
294 | }
295 |
296 | // SaveX is like Save, but panics if an error occurs.
297 | func (tcb *TaskCreateBulk) SaveX(ctx context.Context) []*Task {
298 | v, err := tcb.Save(ctx)
299 | if err != nil {
300 | panic(err)
301 | }
302 | return v
303 | }
304 |
--------------------------------------------------------------------------------
/app/gen/models/task_delete.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "context"
7 | "fmt"
8 |
9 | "entgo.io/ent/dialect/sql"
10 | "entgo.io/ent/dialect/sql/sqlgraph"
11 | "entgo.io/ent/schema/field"
12 | "github.com/adnaan/gomodest-starter/app/gen/models/predicate"
13 | "github.com/adnaan/gomodest-starter/app/gen/models/task"
14 | )
15 |
16 | // TaskDelete is the builder for deleting a Task entity.
17 | type TaskDelete struct {
18 | config
19 | hooks []Hook
20 | mutation *TaskMutation
21 | }
22 |
23 | // Where adds a new predicate to the TaskDelete builder.
24 | func (td *TaskDelete) Where(ps ...predicate.Task) *TaskDelete {
25 | td.mutation.predicates = append(td.mutation.predicates, ps...)
26 | return td
27 | }
28 |
29 | // Exec executes the deletion query and returns how many vertices were deleted.
30 | func (td *TaskDelete) Exec(ctx context.Context) (int, error) {
31 | var (
32 | err error
33 | affected int
34 | )
35 | if len(td.hooks) == 0 {
36 | affected, err = td.sqlExec(ctx)
37 | } else {
38 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
39 | mutation, ok := m.(*TaskMutation)
40 | if !ok {
41 | return nil, fmt.Errorf("unexpected mutation type %T", m)
42 | }
43 | td.mutation = mutation
44 | affected, err = td.sqlExec(ctx)
45 | mutation.done = true
46 | return affected, err
47 | })
48 | for i := len(td.hooks) - 1; i >= 0; i-- {
49 | mut = td.hooks[i](mut)
50 | }
51 | if _, err := mut.Mutate(ctx, td.mutation); err != nil {
52 | return 0, err
53 | }
54 | }
55 | return affected, err
56 | }
57 |
58 | // ExecX is like Exec, but panics if an error occurs.
59 | func (td *TaskDelete) ExecX(ctx context.Context) int {
60 | n, err := td.Exec(ctx)
61 | if err != nil {
62 | panic(err)
63 | }
64 | return n
65 | }
66 |
67 | func (td *TaskDelete) sqlExec(ctx context.Context) (int, error) {
68 | _spec := &sqlgraph.DeleteSpec{
69 | Node: &sqlgraph.NodeSpec{
70 | Table: task.Table,
71 | ID: &sqlgraph.FieldSpec{
72 | Type: field.TypeString,
73 | Column: task.FieldID,
74 | },
75 | },
76 | }
77 | if ps := td.mutation.predicates; len(ps) > 0 {
78 | _spec.Predicate = func(selector *sql.Selector) {
79 | for i := range ps {
80 | ps[i](selector)
81 | }
82 | }
83 | }
84 | return sqlgraph.DeleteNodes(ctx, td.driver, _spec)
85 | }
86 |
87 | // TaskDeleteOne is the builder for deleting a single Task entity.
88 | type TaskDeleteOne struct {
89 | td *TaskDelete
90 | }
91 |
92 | // Exec executes the deletion query.
93 | func (tdo *TaskDeleteOne) Exec(ctx context.Context) error {
94 | n, err := tdo.td.Exec(ctx)
95 | switch {
96 | case err != nil:
97 | return err
98 | case n == 0:
99 | return &NotFoundError{task.Label}
100 | default:
101 | return nil
102 | }
103 | }
104 |
105 | // ExecX is like Exec, but panics if an error occurs.
106 | func (tdo *TaskDeleteOne) ExecX(ctx context.Context) {
107 | tdo.td.ExecX(ctx)
108 | }
109 |
--------------------------------------------------------------------------------
/app/gen/models/task_update.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "time"
9 |
10 | "entgo.io/ent/dialect/sql"
11 | "entgo.io/ent/dialect/sql/sqlgraph"
12 | "entgo.io/ent/schema/field"
13 | "github.com/adnaan/gomodest-starter/app/gen/models/predicate"
14 | "github.com/adnaan/gomodest-starter/app/gen/models/task"
15 | )
16 |
17 | // TaskUpdate is the builder for updating Task entities.
18 | type TaskUpdate struct {
19 | config
20 | hooks []Hook
21 | mutation *TaskMutation
22 | }
23 |
24 | // Where adds a new predicate for the TaskUpdate builder.
25 | func (tu *TaskUpdate) Where(ps ...predicate.Task) *TaskUpdate {
26 | tu.mutation.predicates = append(tu.mutation.predicates, ps...)
27 | return tu
28 | }
29 |
30 | // SetOwner sets the "owner" field.
31 | func (tu *TaskUpdate) SetOwner(s string) *TaskUpdate {
32 | tu.mutation.SetOwner(s)
33 | return tu
34 | }
35 |
36 | // SetText sets the "text" field.
37 | func (tu *TaskUpdate) SetText(s string) *TaskUpdate {
38 | tu.mutation.SetText(s)
39 | return tu
40 | }
41 |
42 | // SetStatus sets the "status" field.
43 | func (tu *TaskUpdate) SetStatus(t task.Status) *TaskUpdate {
44 | tu.mutation.SetStatus(t)
45 | return tu
46 | }
47 |
48 | // SetNillableStatus sets the "status" field if the given value is not nil.
49 | func (tu *TaskUpdate) SetNillableStatus(t *task.Status) *TaskUpdate {
50 | if t != nil {
51 | tu.SetStatus(*t)
52 | }
53 | return tu
54 | }
55 |
56 | // ClearStatus clears the value of the "status" field.
57 | func (tu *TaskUpdate) ClearStatus() *TaskUpdate {
58 | tu.mutation.ClearStatus()
59 | return tu
60 | }
61 |
62 | // SetUpdatedAt sets the "updated_at" field.
63 | func (tu *TaskUpdate) SetUpdatedAt(t time.Time) *TaskUpdate {
64 | tu.mutation.SetUpdatedAt(t)
65 | return tu
66 | }
67 |
68 | // Mutation returns the TaskMutation object of the builder.
69 | func (tu *TaskUpdate) Mutation() *TaskMutation {
70 | return tu.mutation
71 | }
72 |
73 | // Save executes the query and returns the number of nodes affected by the update operation.
74 | func (tu *TaskUpdate) Save(ctx context.Context) (int, error) {
75 | var (
76 | err error
77 | affected int
78 | )
79 | tu.defaults()
80 | if len(tu.hooks) == 0 {
81 | if err = tu.check(); err != nil {
82 | return 0, err
83 | }
84 | affected, err = tu.sqlSave(ctx)
85 | } else {
86 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
87 | mutation, ok := m.(*TaskMutation)
88 | if !ok {
89 | return nil, fmt.Errorf("unexpected mutation type %T", m)
90 | }
91 | if err = tu.check(); err != nil {
92 | return 0, err
93 | }
94 | tu.mutation = mutation
95 | affected, err = tu.sqlSave(ctx)
96 | mutation.done = true
97 | return affected, err
98 | })
99 | for i := len(tu.hooks) - 1; i >= 0; i-- {
100 | mut = tu.hooks[i](mut)
101 | }
102 | if _, err := mut.Mutate(ctx, tu.mutation); err != nil {
103 | return 0, err
104 | }
105 | }
106 | return affected, err
107 | }
108 |
109 | // SaveX is like Save, but panics if an error occurs.
110 | func (tu *TaskUpdate) SaveX(ctx context.Context) int {
111 | affected, err := tu.Save(ctx)
112 | if err != nil {
113 | panic(err)
114 | }
115 | return affected
116 | }
117 |
118 | // Exec executes the query.
119 | func (tu *TaskUpdate) Exec(ctx context.Context) error {
120 | _, err := tu.Save(ctx)
121 | return err
122 | }
123 |
124 | // ExecX is like Exec, but panics if an error occurs.
125 | func (tu *TaskUpdate) ExecX(ctx context.Context) {
126 | if err := tu.Exec(ctx); err != nil {
127 | panic(err)
128 | }
129 | }
130 |
131 | // defaults sets the default values of the builder before save.
132 | func (tu *TaskUpdate) defaults() {
133 | if _, ok := tu.mutation.UpdatedAt(); !ok {
134 | v := task.UpdateDefaultUpdatedAt()
135 | tu.mutation.SetUpdatedAt(v)
136 | }
137 | }
138 |
139 | // check runs all checks and user-defined validators on the builder.
140 | func (tu *TaskUpdate) check() error {
141 | if v, ok := tu.mutation.Status(); ok {
142 | if err := task.StatusValidator(v); err != nil {
143 | return &ValidationError{Name: "status", err: fmt.Errorf("models: validator failed for field \"status\": %w", err)}
144 | }
145 | }
146 | return nil
147 | }
148 |
149 | func (tu *TaskUpdate) sqlSave(ctx context.Context) (n int, err error) {
150 | _spec := &sqlgraph.UpdateSpec{
151 | Node: &sqlgraph.NodeSpec{
152 | Table: task.Table,
153 | Columns: task.Columns,
154 | ID: &sqlgraph.FieldSpec{
155 | Type: field.TypeString,
156 | Column: task.FieldID,
157 | },
158 | },
159 | }
160 | if ps := tu.mutation.predicates; len(ps) > 0 {
161 | _spec.Predicate = func(selector *sql.Selector) {
162 | for i := range ps {
163 | ps[i](selector)
164 | }
165 | }
166 | }
167 | if value, ok := tu.mutation.Owner(); ok {
168 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
169 | Type: field.TypeString,
170 | Value: value,
171 | Column: task.FieldOwner,
172 | })
173 | }
174 | if value, ok := tu.mutation.Text(); ok {
175 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
176 | Type: field.TypeString,
177 | Value: value,
178 | Column: task.FieldText,
179 | })
180 | }
181 | if value, ok := tu.mutation.Status(); ok {
182 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
183 | Type: field.TypeEnum,
184 | Value: value,
185 | Column: task.FieldStatus,
186 | })
187 | }
188 | if tu.mutation.StatusCleared() {
189 | _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
190 | Type: field.TypeEnum,
191 | Column: task.FieldStatus,
192 | })
193 | }
194 | if value, ok := tu.mutation.UpdatedAt(); ok {
195 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
196 | Type: field.TypeTime,
197 | Value: value,
198 | Column: task.FieldUpdatedAt,
199 | })
200 | }
201 | if n, err = sqlgraph.UpdateNodes(ctx, tu.driver, _spec); err != nil {
202 | if _, ok := err.(*sqlgraph.NotFoundError); ok {
203 | err = &NotFoundError{task.Label}
204 | } else if cerr, ok := isSQLConstraintError(err); ok {
205 | err = cerr
206 | }
207 | return 0, err
208 | }
209 | return n, nil
210 | }
211 |
212 | // TaskUpdateOne is the builder for updating a single Task entity.
213 | type TaskUpdateOne struct {
214 | config
215 | hooks []Hook
216 | mutation *TaskMutation
217 | }
218 |
219 | // SetOwner sets the "owner" field.
220 | func (tuo *TaskUpdateOne) SetOwner(s string) *TaskUpdateOne {
221 | tuo.mutation.SetOwner(s)
222 | return tuo
223 | }
224 |
225 | // SetText sets the "text" field.
226 | func (tuo *TaskUpdateOne) SetText(s string) *TaskUpdateOne {
227 | tuo.mutation.SetText(s)
228 | return tuo
229 | }
230 |
231 | // SetStatus sets the "status" field.
232 | func (tuo *TaskUpdateOne) SetStatus(t task.Status) *TaskUpdateOne {
233 | tuo.mutation.SetStatus(t)
234 | return tuo
235 | }
236 |
237 | // SetNillableStatus sets the "status" field if the given value is not nil.
238 | func (tuo *TaskUpdateOne) SetNillableStatus(t *task.Status) *TaskUpdateOne {
239 | if t != nil {
240 | tuo.SetStatus(*t)
241 | }
242 | return tuo
243 | }
244 |
245 | // ClearStatus clears the value of the "status" field.
246 | func (tuo *TaskUpdateOne) ClearStatus() *TaskUpdateOne {
247 | tuo.mutation.ClearStatus()
248 | return tuo
249 | }
250 |
251 | // SetUpdatedAt sets the "updated_at" field.
252 | func (tuo *TaskUpdateOne) SetUpdatedAt(t time.Time) *TaskUpdateOne {
253 | tuo.mutation.SetUpdatedAt(t)
254 | return tuo
255 | }
256 |
257 | // Mutation returns the TaskMutation object of the builder.
258 | func (tuo *TaskUpdateOne) Mutation() *TaskMutation {
259 | return tuo.mutation
260 | }
261 |
262 | // Save executes the query and returns the updated Task entity.
263 | func (tuo *TaskUpdateOne) Save(ctx context.Context) (*Task, error) {
264 | var (
265 | err error
266 | node *Task
267 | )
268 | tuo.defaults()
269 | if len(tuo.hooks) == 0 {
270 | if err = tuo.check(); err != nil {
271 | return nil, err
272 | }
273 | node, err = tuo.sqlSave(ctx)
274 | } else {
275 | var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
276 | mutation, ok := m.(*TaskMutation)
277 | if !ok {
278 | return nil, fmt.Errorf("unexpected mutation type %T", m)
279 | }
280 | if err = tuo.check(); err != nil {
281 | return nil, err
282 | }
283 | tuo.mutation = mutation
284 | node, err = tuo.sqlSave(ctx)
285 | mutation.done = true
286 | return node, err
287 | })
288 | for i := len(tuo.hooks) - 1; i >= 0; i-- {
289 | mut = tuo.hooks[i](mut)
290 | }
291 | if _, err := mut.Mutate(ctx, tuo.mutation); err != nil {
292 | return nil, err
293 | }
294 | }
295 | return node, err
296 | }
297 |
298 | // SaveX is like Save, but panics if an error occurs.
299 | func (tuo *TaskUpdateOne) SaveX(ctx context.Context) *Task {
300 | node, err := tuo.Save(ctx)
301 | if err != nil {
302 | panic(err)
303 | }
304 | return node
305 | }
306 |
307 | // Exec executes the query on the entity.
308 | func (tuo *TaskUpdateOne) Exec(ctx context.Context) error {
309 | _, err := tuo.Save(ctx)
310 | return err
311 | }
312 |
313 | // ExecX is like Exec, but panics if an error occurs.
314 | func (tuo *TaskUpdateOne) ExecX(ctx context.Context) {
315 | if err := tuo.Exec(ctx); err != nil {
316 | panic(err)
317 | }
318 | }
319 |
320 | // defaults sets the default values of the builder before save.
321 | func (tuo *TaskUpdateOne) defaults() {
322 | if _, ok := tuo.mutation.UpdatedAt(); !ok {
323 | v := task.UpdateDefaultUpdatedAt()
324 | tuo.mutation.SetUpdatedAt(v)
325 | }
326 | }
327 |
328 | // check runs all checks and user-defined validators on the builder.
329 | func (tuo *TaskUpdateOne) check() error {
330 | if v, ok := tuo.mutation.Status(); ok {
331 | if err := task.StatusValidator(v); err != nil {
332 | return &ValidationError{Name: "status", err: fmt.Errorf("models: validator failed for field \"status\": %w", err)}
333 | }
334 | }
335 | return nil
336 | }
337 |
338 | func (tuo *TaskUpdateOne) sqlSave(ctx context.Context) (_node *Task, err error) {
339 | _spec := &sqlgraph.UpdateSpec{
340 | Node: &sqlgraph.NodeSpec{
341 | Table: task.Table,
342 | Columns: task.Columns,
343 | ID: &sqlgraph.FieldSpec{
344 | Type: field.TypeString,
345 | Column: task.FieldID,
346 | },
347 | },
348 | }
349 | id, ok := tuo.mutation.ID()
350 | if !ok {
351 | return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing Task.ID for update")}
352 | }
353 | _spec.Node.ID.Value = id
354 | if ps := tuo.mutation.predicates; len(ps) > 0 {
355 | _spec.Predicate = func(selector *sql.Selector) {
356 | for i := range ps {
357 | ps[i](selector)
358 | }
359 | }
360 | }
361 | if value, ok := tuo.mutation.Owner(); ok {
362 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
363 | Type: field.TypeString,
364 | Value: value,
365 | Column: task.FieldOwner,
366 | })
367 | }
368 | if value, ok := tuo.mutation.Text(); ok {
369 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
370 | Type: field.TypeString,
371 | Value: value,
372 | Column: task.FieldText,
373 | })
374 | }
375 | if value, ok := tuo.mutation.Status(); ok {
376 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
377 | Type: field.TypeEnum,
378 | Value: value,
379 | Column: task.FieldStatus,
380 | })
381 | }
382 | if tuo.mutation.StatusCleared() {
383 | _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{
384 | Type: field.TypeEnum,
385 | Column: task.FieldStatus,
386 | })
387 | }
388 | if value, ok := tuo.mutation.UpdatedAt(); ok {
389 | _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{
390 | Type: field.TypeTime,
391 | Value: value,
392 | Column: task.FieldUpdatedAt,
393 | })
394 | }
395 | _node = &Task{config: tuo.config}
396 | _spec.Assign = _node.assignValues
397 | _spec.ScanValues = _node.scanValues
398 | if err = sqlgraph.UpdateNode(ctx, tuo.driver, _spec); err != nil {
399 | if _, ok := err.(*sqlgraph.NotFoundError); ok {
400 | err = &NotFoundError{task.Label}
401 | } else if cerr, ok := isSQLConstraintError(err); ok {
402 | err = cerr
403 | }
404 | return nil, err
405 | }
406 | return _node, nil
407 | }
408 |
--------------------------------------------------------------------------------
/app/gen/models/tx.go:
--------------------------------------------------------------------------------
1 | // Code generated (@generated) by entc, DO NOT EDIT.
2 |
3 | package models
4 |
5 | import (
6 | "context"
7 | "sync"
8 |
9 | "entgo.io/ent/dialect"
10 | )
11 |
12 | // Tx is a transactional client that is created by calling Client.Tx().
13 | type Tx struct {
14 | config
15 | // Task is the client for interacting with the Task builders.
16 | Task *TaskClient
17 |
18 | // lazily loaded.
19 | client *Client
20 | clientOnce sync.Once
21 |
22 | // completion callbacks.
23 | mu sync.Mutex
24 | onCommit []CommitHook
25 | onRollback []RollbackHook
26 |
27 | // ctx lives for the life of the transaction. It is
28 | // the same context used by the underlying connection.
29 | ctx context.Context
30 | }
31 |
32 | type (
33 | // Committer is the interface that wraps the Committer method.
34 | Committer interface {
35 | Commit(context.Context, *Tx) error
36 | }
37 |
38 | // The CommitFunc type is an adapter to allow the use of ordinary
39 | // function as a Committer. If f is a function with the appropriate
40 | // signature, CommitFunc(f) is a Committer that calls f.
41 | CommitFunc func(context.Context, *Tx) error
42 |
43 | // CommitHook defines the "commit middleware". A function that gets a Committer
44 | // and returns a Committer. For example:
45 | //
46 | // hook := func(next ent.Committer) ent.Committer {
47 | // return ent.CommitFunc(func(context.Context, tx *ent.Tx) error {
48 | // // Do some stuff before.
49 | // if err := next.Commit(ctx, tx); err != nil {
50 | // return err
51 | // }
52 | // // Do some stuff after.
53 | // return nil
54 | // })
55 | // }
56 | //
57 | CommitHook func(Committer) Committer
58 | )
59 |
60 | // Commit calls f(ctx, m).
61 | func (f CommitFunc) Commit(ctx context.Context, tx *Tx) error {
62 | return f(ctx, tx)
63 | }
64 |
65 | // Commit commits the transaction.
66 | func (tx *Tx) Commit() error {
67 | txDriver := tx.config.driver.(*txDriver)
68 | var fn Committer = CommitFunc(func(context.Context, *Tx) error {
69 | return txDriver.tx.Commit()
70 | })
71 | tx.mu.Lock()
72 | hooks := append([]CommitHook(nil), tx.onCommit...)
73 | tx.mu.Unlock()
74 | for i := len(hooks) - 1; i >= 0; i-- {
75 | fn = hooks[i](fn)
76 | }
77 | return fn.Commit(tx.ctx, tx)
78 | }
79 |
80 | // OnCommit adds a hook to call on commit.
81 | func (tx *Tx) OnCommit(f CommitHook) {
82 | tx.mu.Lock()
83 | defer tx.mu.Unlock()
84 | tx.onCommit = append(tx.onCommit, f)
85 | }
86 |
87 | type (
88 | // Rollbacker is the interface that wraps the Rollbacker method.
89 | Rollbacker interface {
90 | Rollback(context.Context, *Tx) error
91 | }
92 |
93 | // The RollbackFunc type is an adapter to allow the use of ordinary
94 | // function as a Rollbacker. If f is a function with the appropriate
95 | // signature, RollbackFunc(f) is a Rollbacker that calls f.
96 | RollbackFunc func(context.Context, *Tx) error
97 |
98 | // RollbackHook defines the "rollback middleware". A function that gets a Rollbacker
99 | // and returns a Rollbacker. For example:
100 | //
101 | // hook := func(next ent.Rollbacker) ent.Rollbacker {
102 | // return ent.RollbackFunc(func(context.Context, tx *ent.Tx) error {
103 | // // Do some stuff before.
104 | // if err := next.Rollback(ctx, tx); err != nil {
105 | // return err
106 | // }
107 | // // Do some stuff after.
108 | // return nil
109 | // })
110 | // }
111 | //
112 | RollbackHook func(Rollbacker) Rollbacker
113 | )
114 |
115 | // Rollback calls f(ctx, m).
116 | func (f RollbackFunc) Rollback(ctx context.Context, tx *Tx) error {
117 | return f(ctx, tx)
118 | }
119 |
120 | // Rollback rollbacks the transaction.
121 | func (tx *Tx) Rollback() error {
122 | txDriver := tx.config.driver.(*txDriver)
123 | var fn Rollbacker = RollbackFunc(func(context.Context, *Tx) error {
124 | return txDriver.tx.Rollback()
125 | })
126 | tx.mu.Lock()
127 | hooks := append([]RollbackHook(nil), tx.onRollback...)
128 | tx.mu.Unlock()
129 | for i := len(hooks) - 1; i >= 0; i-- {
130 | fn = hooks[i](fn)
131 | }
132 | return fn.Rollback(tx.ctx, tx)
133 | }
134 |
135 | // OnRollback adds a hook to call on rollback.
136 | func (tx *Tx) OnRollback(f RollbackHook) {
137 | tx.mu.Lock()
138 | defer tx.mu.Unlock()
139 | tx.onRollback = append(tx.onRollback, f)
140 | }
141 |
142 | // Client returns a Client that binds to current transaction.
143 | func (tx *Tx) Client() *Client {
144 | tx.clientOnce.Do(func() {
145 | tx.client = &Client{config: tx.config}
146 | tx.client.init()
147 | })
148 | return tx.client
149 | }
150 |
151 | func (tx *Tx) init() {
152 | tx.Task = NewTaskClient(tx.config)
153 | }
154 |
155 | // txDriver wraps the given dialect.Tx with a nop dialect.Driver implementation.
156 | // The idea is to support transactions without adding any extra code to the builders.
157 | // When a builder calls to driver.Tx(), it gets the same dialect.Tx instance.
158 | // Commit and Rollback are nop for the internal builders and the user must call one
159 | // of them in order to commit or rollback the transaction.
160 | //
161 | // If a closed transaction is embedded in one of the generated entities, and the entity
162 | // applies a query, for example: Task.QueryXXX(), the query will be executed
163 | // through the driver which created this transaction.
164 | //
165 | // Note that txDriver is not goroutine safe.
166 | type txDriver struct {
167 | // the driver we started the transaction from.
168 | drv dialect.Driver
169 | // tx is the underlying transaction.
170 | tx dialect.Tx
171 | }
172 |
173 | // newTx creates a new transactional driver.
174 | func newTx(ctx context.Context, drv dialect.Driver) (*txDriver, error) {
175 | tx, err := drv.Tx(ctx)
176 | if err != nil {
177 | return nil, err
178 | }
179 | return &txDriver{tx: tx, drv: drv}, nil
180 | }
181 |
182 | // Tx returns the transaction wrapper (txDriver) to avoid Commit or Rollback calls
183 | // from the internal builders. Should be called only by the internal builders.
184 | func (tx *txDriver) Tx(context.Context) (dialect.Tx, error) { return tx, nil }
185 |
186 | // Dialect returns the dialect of the driver we started the transaction from.
187 | func (tx *txDriver) Dialect() string { return tx.drv.Dialect() }
188 |
189 | // Close is a nop close.
190 | func (*txDriver) Close() error { return nil }
191 |
192 | // Commit is a nop commit for the internal builders.
193 | // User must call `Tx.Commit` in order to commit the transaction.
194 | func (*txDriver) Commit() error { return nil }
195 |
196 | // Rollback is a nop rollback for the internal builders.
197 | // User must call `Tx.Rollback` in order to rollback the transaction.
198 | func (*txDriver) Rollback() error { return nil }
199 |
200 | // Exec calls tx.Exec.
201 | func (tx *txDriver) Exec(ctx context.Context, query string, args, v interface{}) error {
202 | return tx.tx.Exec(ctx, query, args, v)
203 | }
204 |
205 | // Query calls tx.Query.
206 | func (tx *txDriver) Query(ctx context.Context, query string, args, v interface{}) error {
207 | return tx.tx.Query(ctx, query, args, v)
208 | }
209 |
210 | var _ dialect.Driver = (*txDriver)(nil)
211 |
--------------------------------------------------------------------------------
/app/generator/entc.go:
--------------------------------------------------------------------------------
1 | // +build ignore
2 |
3 | package main
4 |
5 | import (
6 | "log"
7 |
8 | "entgo.io/ent/entc"
9 | "entgo.io/ent/entc/gen"
10 | "entgo.io/ent/schema/field"
11 | )
12 |
13 | func main() {
14 | err := entc.Generate("../schema", &gen.Config{
15 | Header: `
16 | // Code generated (@generated) by entc, DO NOT EDIT.
17 | `,
18 | IDType: &field.TypeInfo{Type: field.TypeInt},
19 | Target: "../gen/models",
20 | Package: "github.com/adnaan/gomodest-starter/app/gen/models",
21 | })
22 | if err != nil {
23 | log.Fatal("running ent codegen:", err)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/generator/generate.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import _ "entgo.io/ent/entc"
4 |
5 | //go:generate go run entc.go
6 |
--------------------------------------------------------------------------------
/app/router.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "net/http"
8 |
9 | "github.com/hako/branca"
10 |
11 | "github.com/adnaan/authn"
12 |
13 | "github.com/adnaan/gomodest-starter/app/gen/models"
14 |
15 | "github.com/go-playground/form"
16 |
17 | "github.com/stripe/stripe-go/v72"
18 |
19 | "github.com/markbates/goth"
20 | "github.com/markbates/goth/providers/google"
21 |
22 | "github.com/go-chi/httplog"
23 |
24 | rl "github.com/adnaan/renderlayout"
25 | "github.com/go-chi/chi"
26 | "github.com/go-chi/chi/middleware"
27 | _ "github.com/mattn/go-sqlite3"
28 | )
29 |
30 | type Context struct {
31 | authn *authn.API
32 | cfg Config
33 | formDecoder *form.Decoder
34 | db *models.Client
35 | branca *branca.Branca
36 | }
37 |
38 | type APIRoute struct {
39 | Method string
40 | Pattern string
41 | HandlerFunc http.HandlerFunc
42 | }
43 |
44 | func Router(ctx context.Context, cfg Config) chi.Router {
45 | //driver := "postgres"
46 | //dataSource := "host=0.0.0.0 port=5432 user=gomodest-starter dbname=gomodest-starter sslmode=disable"
47 | stripe.Key = cfg.StripeSecretKey
48 |
49 | db, err := models.Open(cfg.Driver, cfg.DataSource)
50 | if err != nil {
51 | panic(err)
52 | }
53 | if err := db.Schema.Create(ctx); err != nil {
54 | panic(err)
55 | }
56 |
57 | appCtx := Context{
58 | db: db,
59 | cfg: cfg,
60 | formDecoder: form.NewDecoder(),
61 | branca: branca.NewBranca(cfg.APIMasterSecret),
62 | }
63 |
64 | authnConfig := authn.Config{
65 | Driver: cfg.Driver,
66 | Datasource: cfg.DataSource,
67 | SessionSecret: cfg.SessionSecret,
68 | SendMail: sendEmailFunc(cfg),
69 | GothProviders: []goth.Provider{
70 | google.New(
71 | cfg.GoogleClientID,
72 | cfg.GoogleSecret,
73 | fmt.Sprintf("%s/auth/callback?provider=google", cfg.Domain),
74 | "email", "profile",
75 | ),
76 | },
77 | }
78 |
79 | appCtx.authn = authn.New(ctx, authnConfig)
80 |
81 | // logger
82 | logger := httplog.NewLogger(cfg.Name,
83 | httplog.Options{
84 | JSON: cfg.LogFormatJSON,
85 | LogLevel: cfg.LogLevel,
86 | })
87 |
88 | index, err := rl.New(
89 | rl.Layout("index"),
90 | rl.DisableCache(true),
91 | rl.Debug(false),
92 | rl.DefaultData(defaultPageHandler(appCtx)),
93 | )
94 |
95 | if err != nil {
96 | log.Fatal(err)
97 | }
98 |
99 | // middlewares
100 | r := chi.NewRouter()
101 | r.Use(middleware.Compress(5))
102 | r.Use(middleware.Heartbeat(cfg.HealthPath))
103 | r.Use(middleware.Recoverer)
104 | r.Use(httplog.RequestLogger(logger))
105 | r.NotFound(index("404"))
106 | // public
107 | r.Route("/", func(r chi.Router) {
108 | r.Post("/webhook/{source}", handleWebhook(appCtx))
109 | r.Get("/", index("home"))
110 | r.Get("/signup", index("account/signup"))
111 | r.Post("/signup", index("account/signup", signupPage(appCtx)))
112 | r.Get("/confirm/{token}", index("account/confirmed", confirmEmailPage(appCtx)))
113 | r.Get("/login", index("account/login", loginPage(appCtx)))
114 | r.Post("/login", index("account/login", loginPageSubmit(appCtx)))
115 | r.Get("/auth/callback", index("account/login", loginProviderCallbackPage(appCtx)))
116 | r.Get("/auth", index("account/login", loginProviderPage(appCtx)))
117 | r.Get("/magic-link-sent", index("account/magic"))
118 | r.Get("/magic-login/{otp}", index("account/login", magicLinkLoginConfirm(appCtx)))
119 | r.Get("/forgot", index("account/forgot"))
120 | r.Post("/forgot", index("account/forgot", forgotPage(appCtx)))
121 | r.Get("/reset/{token}", index("account/reset"))
122 | r.Post("/reset/{token}", index("account/reset", resetPage(appCtx)))
123 | r.Get("/logout", func(w http.ResponseWriter, r *http.Request) {
124 | acc, err := appCtx.authn.CurrentAccount(r)
125 | if err != nil {
126 | log.Println("err logging out ", err)
127 | http.Redirect(w, r, "/", http.StatusSeeOther)
128 | }
129 | acc.Logout(w, r)
130 | })
131 | r.Get("/change/{token}", index("account/changed", confirmEmailChangePage(appCtx)))
132 | })
133 |
134 | // authenticated
135 | r.Route("/account", func(r chi.Router) {
136 | r.Use(appCtx.authn.IsAuthenticated)
137 | r.Get("/", index("account/main", accountPage(appCtx)))
138 | r.Post("/", index("account/main", accountPageSubmit(appCtx)))
139 | r.Post("/delete", index("account/main", deleteAccount(appCtx)))
140 |
141 | r.Post("/checkout", handleCreateCheckoutSession(appCtx))
142 | r.Get("/checkout/success", handleCheckoutSuccess(appCtx))
143 | r.Get("/checkout/cancel", handleCheckoutCancel(appCtx))
144 | r.Get("/subscription/manage", handleManageSubscription(appCtx))
145 | })
146 |
147 | r.Route("/app", func(r chi.Router) {
148 | r.Use(appCtx.authn.IsAuthenticated)
149 | r.Get("/", index("app", listTasks(appCtx)))
150 | r.Post("/tasks/new", index("app", createNewTask(appCtx), listTasks(appCtx)))
151 | r.Post("/tasks/{id}/edit", index("app", editTask(appCtx), listTasks(appCtx)))
152 | r.Post("/tasks/{id}/delete", index("app", deleteTask(appCtx), listTasks(appCtx)))
153 | })
154 |
155 | r.Route("/api", func(r chi.Router) {
156 | r.Use(appCtx.authn.IsAuthenticated)
157 | r.Use(middleware.AllowContentType("application/json"))
158 | r.Route("/tasks", func(r chi.Router) {
159 | r.Get("/", list(appCtx))
160 | r.Post("/", create(appCtx))
161 | })
162 | r.Route("/tasks/{id}", func(r chi.Router) {
163 | r.Put("/status", updateStatus(appCtx))
164 | r.Put("/text", updateText(appCtx))
165 | r.Delete("/", delete(appCtx))
166 | })
167 | })
168 |
169 | return r
170 | }
171 |
--------------------------------------------------------------------------------
/app/schema/task.go:
--------------------------------------------------------------------------------
1 | package schema
2 |
3 | import (
4 | "time"
5 |
6 | "entgo.io/ent/dialect/entsql"
7 | "entgo.io/ent/schema"
8 |
9 | "entgo.io/ent"
10 | "entgo.io/ent/schema/field"
11 | )
12 |
13 | // Task holds the schema definition for the Task entity.
14 | type Task struct {
15 | ent.Schema
16 | }
17 |
18 | func (Task) Annotations() []schema.Annotation {
19 | return []schema.Annotation{
20 | entsql.Annotation{Table: "tasks"},
21 | }
22 | }
23 |
24 | // Fields of the Task.
25 | func (Task) Fields() []ent.Field {
26 | return []ent.Field{
27 | field.String("id"),
28 | field.String("owner"),
29 | field.Text("text"),
30 | field.Enum("status").Values("todo", "inprogress", "done").Default("todo").Optional(),
31 | field.Time("created_at").Immutable().Default(time.Now),
32 | field.Time("updated_at").Default(time.Now).UpdateDefault(time.Now),
33 | }
34 | }
35 |
36 | // Edges of the Task.
37 | func (Task) Edges() []ent.Edge {
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/app/stripe.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/stripe/stripe-go/v72"
9 | portalsession "github.com/stripe/stripe-go/v72/billingportal/session"
10 | "github.com/stripe/stripe-go/v72/checkout/session"
11 |
12 | "github.com/go-chi/render"
13 | )
14 |
15 | // modified from https://github.com/stripe-samples/checkout-single-subscription/blob/master/server/go/server.go
16 |
17 | const (
18 | billingIDKey = "billing_id"
19 | currentPriceIDKey = "current_price_id"
20 | )
21 |
22 | type errResponse struct {
23 | Error string `json:"error"`
24 | }
25 |
26 | func handleCreateCheckoutSession(appCtx Context) http.HandlerFunc {
27 |
28 | type req struct {
29 | Price string `json:"price"`
30 | }
31 | return func(w http.ResponseWriter, r *http.Request) {
32 | req := new(req)
33 | account, err := appCtx.authn.CurrentAccount(r)
34 | if err != nil {
35 | log.Printf("authn.CurrentAccount: %v", err)
36 | render.Status(r, http.StatusUnauthorized)
37 | render.JSON(w, r, &errResponse{"unauthorized"})
38 | return
39 | }
40 |
41 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
42 | log.Printf("json.NewDecoder.Decode: %v", err)
43 | render.Status(r, http.StatusBadRequest)
44 | render.JSON(w, r, &errResponse{err.Error()})
45 | return
46 | }
47 |
48 | params := &stripe.CheckoutSessionParams{
49 | CustomerEmail: stripe.String(account.Email()),
50 | SuccessURL: stripe.String(appCtx.cfg.Domain + "/account/checkout/success?session_id={CHECKOUT_SESSION_ID}"),
51 | CancelURL: stripe.String(appCtx.cfg.Domain + "/account/checkout/cancel"),
52 | PaymentMethodTypes: stripe.StringSlice([]string{
53 | "card",
54 | }),
55 | Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)),
56 | LineItems: []*stripe.CheckoutSessionLineItemParams{
57 | {
58 | Price: stripe.String(req.Price),
59 | Quantity: stripe.Int64(1),
60 | },
61 | },
62 | }
63 |
64 | s, err := session.New(params)
65 | if err != nil {
66 | log.Printf("session.New: %v", err)
67 | render.Status(r, http.StatusInternalServerError)
68 | render.JSON(w, r, &errResponse{err.Error()})
69 | return
70 | }
71 |
72 | render.JSON(w, r, struct {
73 | SessionID string `json:"sessionId"`
74 | }{SessionID: s.ID})
75 |
76 | }
77 | }
78 |
79 | func handleCheckoutSuccess(appCtx Context) http.HandlerFunc {
80 | return func(w http.ResponseWriter, r *http.Request) {
81 | sessionID := r.URL.Query().Get("session_id")
82 | if sessionID == "" {
83 | http.Redirect(w, r, "/account", http.StatusSeeOther)
84 | return
85 | }
86 | s, err := session.Get(sessionID, nil)
87 | if err != nil {
88 | log.Printf("session.Get: %v\n", err)
89 | render.Status(r, http.StatusBadRequest)
90 | render.JSON(w, r, &errResponse{err.Error()})
91 | return
92 | }
93 |
94 | account, err := appCtx.authn.CurrentAccount(r)
95 | if err != nil {
96 | log.Printf("authn.CurrentAccount: %v\n", err)
97 | render.Status(r, http.StatusUnauthorized)
98 | render.JSON(w, r, &errResponse{"unauthorized"})
99 | return
100 | }
101 |
102 | err = account.Attributes().Set(billingIDKey, s.Customer.ID)
103 | if err != nil {
104 | log.Printf("UpdateBillingID %v\n", err)
105 | render.Status(r, http.StatusInternalServerError)
106 | render.JSON(w, r, &errResponse{err.Error()})
107 | return
108 | }
109 |
110 | http.Redirect(w, r, "/account?checkout=success", http.StatusSeeOther)
111 | }
112 | }
113 |
114 | func handleCheckoutCancel(appCtx Context) http.HandlerFunc {
115 | return func(w http.ResponseWriter, r *http.Request) {
116 | http.Redirect(w, r, "/account?checkout=cancel", http.StatusSeeOther)
117 | }
118 | }
119 |
120 | func handleManageSubscription(appCtx Context) http.HandlerFunc {
121 | return func(w http.ResponseWriter, r *http.Request) {
122 | account, err := appCtx.authn.CurrentAccount(r)
123 | if err != nil {
124 | log.Printf("authn.CurrentAccount: %v", err)
125 | render.Status(r, http.StatusUnauthorized)
126 | render.JSON(w, r, &errResponse{"unauthorized"})
127 | return
128 | }
129 |
130 | // expect plan to be change
131 | err = account.Attributes().Session().Del(w, currentPriceIDKey)
132 | if err != nil {
133 | log.Printf("account.Attributes().Session().Del(), %s failed err %v\n", currentPriceIDKey, err)
134 | }
135 | billingID, ok := account.Attributes().Map().String(billingIDKey)
136 | if !ok {
137 | log.Printf(" %s not found \n", billingIDKey)
138 | http.Redirect(w, r, "/account", http.StatusSeeOther)
139 | return
140 | }
141 | params := &stripe.BillingPortalSessionParams{
142 | Customer: stripe.String(billingID),
143 | ReturnURL: stripe.String(appCtx.cfg.Domain + "/account"),
144 | }
145 |
146 | ps, err := portalsession.New(params)
147 | if err != nil {
148 | log.Printf("portalsession.New: %v", err)
149 | http.Redirect(w, r, "/account", http.StatusSeeOther)
150 | return
151 | }
152 |
153 | http.Redirect(w, r, ps.URL, http.StatusSeeOther)
154 |
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/app/task_pages.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/adnaan/authn"
8 | "github.com/adnaan/gomodest-starter/app/gen/models/task"
9 | rl "github.com/adnaan/renderlayout"
10 | "github.com/go-chi/chi"
11 | "github.com/lithammer/shortuuid/v3"
12 | )
13 |
14 | func listTasks(appCtx Context) rl.Data {
15 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
16 | userID := authn.AccountIDFromContext(r)
17 | tasks, err := appCtx.db.Task.Query().Where(task.Owner(userID)).All(r.Context())
18 | if err != nil {
19 | return nil, fmt.Errorf("%w", err)
20 | }
21 |
22 | return rl.D{
23 | "tasks": tasks,
24 | }, nil
25 | }
26 | }
27 |
28 | func createNewTask(appCtx Context) rl.Data {
29 | type req struct {
30 | Text string `json:"text"`
31 | }
32 |
33 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
34 | req := new(req)
35 | err := r.ParseForm()
36 | if err != nil {
37 | return nil, fmt.Errorf("%w", err)
38 | }
39 |
40 | err = appCtx.formDecoder.Decode(req, r.Form)
41 | if err != nil {
42 | return nil, fmt.Errorf("%w", err)
43 | }
44 |
45 | if req.Text == "" {
46 | return nil, fmt.Errorf("%w", fmt.Errorf("empty task"))
47 | }
48 |
49 | userID := authn.AccountIDFromContext(r)
50 | _, err = appCtx.db.Task.Create().
51 | SetID(shortuuid.New()).
52 | SetStatus(task.StatusInprogress).
53 | SetOwner(userID).
54 | SetText(req.Text).
55 | Save(r.Context())
56 | if err != nil {
57 | return nil, fmt.Errorf("%w", err)
58 | }
59 |
60 | return nil, nil
61 | }
62 | }
63 |
64 | func deleteTask(appCtx Context) rl.Data {
65 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
66 | id := chi.URLParam(r, "id")
67 | userID := authn.AccountIDFromContext(r)
68 |
69 | _, err := appCtx.db.Task.Delete().Where(task.And(
70 | task.Owner(userID), task.ID(id),
71 | )).Exec(r.Context())
72 | if err != nil {
73 | return nil, fmt.Errorf("%w", err)
74 | }
75 |
76 | return nil, nil
77 | }
78 | }
79 |
80 | func editTask(appCtx Context) rl.Data {
81 | type req struct {
82 | Text string `json:"text"`
83 | }
84 | return func(w http.ResponseWriter, r *http.Request) (rl.D, error) {
85 | req := new(req)
86 | err := r.ParseForm()
87 | if err != nil {
88 | return nil, fmt.Errorf("%w", err)
89 | }
90 |
91 | err = appCtx.formDecoder.Decode(req, r.Form)
92 | if err != nil {
93 | return nil, fmt.Errorf("%w", err)
94 | }
95 |
96 | if req.Text == "" {
97 | return nil, fmt.Errorf("%w", fmt.Errorf("empty task"))
98 | }
99 |
100 | id := chi.URLParam(r, "id")
101 | userID := authn.AccountIDFromContext(r)
102 | err = appCtx.db.Task.Update().Where(task.And(
103 | task.Owner(userID), task.ID(id),
104 | )).SetText(req.Text).Exec(r.Context())
105 | if err != nil {
106 | return nil, fmt.Errorf("%w", err)
107 | }
108 |
109 | return nil, nil
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/app/webhook.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "io/ioutil"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/go-chi/render"
9 |
10 | "github.com/stripe/stripe-go/v72/webhook"
11 |
12 | "github.com/go-chi/chi"
13 | )
14 |
15 | func handleWebhook(appCtx Context) http.HandlerFunc {
16 | return func(w http.ResponseWriter, r *http.Request) {
17 | source := chi.URLParam(r, "source")
18 |
19 | b, err := ioutil.ReadAll(r.Body)
20 | if err != nil {
21 | log.Printf("ioutil.ReadAll: %v", err)
22 | render.Status(r, http.StatusBadRequest)
23 | return
24 | }
25 |
26 | switch source {
27 | case "stripe":
28 | event, err := webhook.ConstructEvent(b, r.Header.Get("Stripe-Signature"), appCtx.cfg.StripeWebhookSecret)
29 | if err != nil {
30 | log.Printf("webhook.ConstructEvent: %v", err)
31 | render.Status(r, http.StatusBadRequest)
32 | return
33 | }
34 |
35 | if event.Type != "checkout.session.completed" {
36 | return
37 | }
38 |
39 | log.Printf("webhook %+v\n", event)
40 | }
41 |
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/assets/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ],
5 | "plugins": [
6 | "@babel/plugin-proposal-class-properties"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gomodest",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "build": "env NODE_ENV=production webpack",
8 | "watch": "env NODE_ENV=development webpack --watch"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "@babel/core": "^7.7.7",
14 | "@babel/plugin-proposal-class-properties": "^7.7.4",
15 | "@babel/preset-env": "^7.7.7",
16 | "@hotwired/turbo": "^7.0.0-beta.4",
17 | "babel-loader": "^8.0.6",
18 | "bulma": "^0.9.1",
19 | "copy-webpack-plugin": "^6.0.2",
20 | "css-loader": "^3.5.3",
21 | "extract-text-webpack-plugin": "^4.0.0-beta.0",
22 | "glob-all": "^3.2.1",
23 | "mini-css-extract-plugin": "^0.9.0",
24 | "node-sass": "^4.14.1",
25 | "optimize-css-assets-webpack-plugin": "^5.0.3",
26 | "purgecss-webpack-plugin": "^2.2.0",
27 | "sass-loader": "^8.0.2",
28 | "stimulus": "^2.0.0",
29 | "stimulus-use": "^0.23.0",
30 | "style-loader": "^1.2.1",
31 | "svelte": "^3.24.1",
32 | "svelte-loader": "^2.13.6",
33 | "watch": "^1.0.2",
34 | "webpack": "^4.43.0",
35 | "webpack-bundle-analyzer": "^3.8.0",
36 | "webpack-cli": "^3.3.11",
37 | "webpack-dev-middleware": "^3.7.2"
38 | },
39 | "dependencies": {
40 | "svelte-tags-input": "^2.6.5"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/assets/src/components/App.svelte:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
45 |
46 |
47 |
todos
48 |
60 |
0}>
61 | {#each todos as todo (todo.id)}
62 |
63 |
64 |
{todo.text}
65 |
66 |
removeTodo(todo.id)}>
67 |
68 |
69 |
70 |
71 |
72 |
73 | {:else}
74 | Nothing here!
76 |
77 | {/each}
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/assets/src/components/Groups.svelte:
--------------------------------------------------------------------------------
1 | List of Groups
--------------------------------------------------------------------------------
/assets/src/components/Guests.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Invite
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/assets/src/components/Members.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Invite
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/assets/src/components/index.js:
--------------------------------------------------------------------------------
1 | import app from "./App.svelte"
2 | import members from "./Members.svelte"
3 | import groups from "./Groups.svelte"
4 | import guests from "./Guests.svelte"
5 |
6 | // export other components here.
7 | export default {
8 | app: app,
9 | members:members,
10 | groups: groups,
11 | guests: guests
12 | }
--------------------------------------------------------------------------------
/assets/src/controllers/account_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 |
3 | export default class extends Controller {
4 | static targets = [ "name","email","magicEmail","password", "confirmPassword", "formError","magic"]
5 |
6 | validateEmail(e) {
7 | if (!this.emailTarget.validity.valid || this.emailTarget.value === ''){
8 | this.showFormError("Invalid Email")
9 | return false;
10 | }
11 | return true;
12 | }
13 |
14 | validateMagicEmail(e) {
15 | if (!this.magicEmailTarget.validity.valid || this.magicEmailTarget.value === ''){
16 | this.showFormError("Invalid Email")
17 | return false;
18 | }
19 | return true;
20 | }
21 |
22 |
23 | validateName() {
24 | if (this.nameTarget.value === ''){
25 | this.showFormError("Name cannot be empty")
26 | return false;
27 | }
28 | return true;
29 | }
30 |
31 | validatePassword() {
32 | if (this.passwordTarget.value === '' || this.passwordTarget.value.length < 8){
33 | this.showFormError("Minimum 8 character password is required")
34 | return false;
35 | }
36 | return true;
37 | }
38 |
39 | submitForgetForm(e){
40 | if (this.validateEmail()){
41 | return;
42 | }
43 | e.preventDefault();
44 | }
45 |
46 | submitSignupForm(e){
47 | if (this.validateEmail() && this.validatePassword() && this.validateName()){
48 | return;
49 | }
50 | e.preventDefault();
51 | }
52 |
53 | submitLoginForm(e){
54 | if (this.validateEmail() && this.validatePassword()){
55 | return;
56 | }
57 | e.preventDefault();
58 | }
59 |
60 | submitMagicLoginForm(e){
61 | if (this.validateMagicEmail()){
62 | return;
63 | }
64 | e.preventDefault();
65 | }
66 |
67 | submitResetForm(e){
68 | if (this.validatePassword() &&(this.passwordTarget.value === this.confirmPasswordTarget.value)){
69 | return;
70 | }
71 |
72 | this.showFormError("Password's don't match")
73 | e.preventDefault();
74 | }
75 |
76 | submitAccountForm(e){
77 | if (this.validateEmail() && this.validateName()){
78 | return;
79 | }
80 | e.preventDefault();
81 | }
82 |
83 | showFormError(message){
84 | this.formErrorTarget.classList.remove("is-hidden")
85 | this.formErrorTarget.innerHTML = message
86 | }
87 |
88 | hideFormError(){
89 | this.formErrorTarget.classList.add("is-hidden")
90 | e.preventDefault();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/assets/src/controllers/clipboard_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 |
3 | export default class extends Controller {
4 | static targets = [ "source", "copied" ]
5 | static classes = [ "copied" ]
6 | copy(e) {
7 | e.preventDefault()
8 | this.sourceTarget.select()
9 | document.execCommand("copy")
10 | this.copiedTarget.classList.remove(this.copiedClass)
11 | }
12 | }
--------------------------------------------------------------------------------
/assets/src/controllers/hover_hidden_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from 'stimulus'
2 | import { useHover } from 'stimulus-use'
3 |
4 | export default class extends Controller {
5 | static targets = ["tools"]
6 | connect() {
7 | useHover(this, { element: this.element });
8 | }
9 |
10 | mouseEnter() {
11 | this.toolsTarget.classList.remove('is-hidden')
12 | }
13 |
14 | mouseLeave() {
15 | // ...
16 | this.toolsTarget.classList.add('is-hidden')
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/assets/src/controllers/navigate_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 |
3 | export default class extends Controller {
4 | static targets = ["modal","toggler","dropup"]
5 | static classes = [ "active" ]
6 |
7 | connect(){
8 |
9 | }
10 |
11 | goto(e){
12 | if (e.currentTarget.dataset.goto){
13 | window.location = e.currentTarget.dataset.goto;
14 | }
15 | }
16 |
17 | goback(e){
18 | window.history.back();
19 | }
20 |
21 | openModal(e){
22 | const targetModal = this.modalTargets.find(i => i.id === e.currentTarget.dataset.modalTargetId);
23 | targetModal.classList.add("is-active")
24 | e.preventDefault();
25 | }
26 |
27 | closeModal(e){
28 | if (e.type === "click"){
29 | const targetModal = this.modalTargets.find(i => i.id === e.currentTarget.dataset.modalTargetId);
30 | targetModal.classList.remove("is-active")
31 | e.preventDefault();
32 | return;
33 | }
34 | }
35 |
36 | keyDown(e){
37 | if (e.keyCode === 27){
38 | this.modalTargets.forEach(item => {
39 | item.classList.remove("is-active")
40 | })
41 | }
42 |
43 | if (e.keyCode === 37){
44 | window.history.back();
45 | }
46 |
47 |
48 | if (e.keyCode === 39){
49 | window.history.forward();
50 | }
51 | }
52 |
53 | toggle(e){
54 | if (!e.currentTarget.dataset.toggleIds){
55 | return;
56 | }
57 | const targetToggleIds = e.currentTarget.dataset.toggleIds.split(",");
58 | const targetToggleClass = e.currentTarget.dataset.toggleClass;
59 | targetToggleIds.forEach(item => {
60 | document.getElementById(item).classList.toggle(targetToggleClass);
61 | })
62 | }
63 |
64 | toggleIsActive(e){
65 | this.dropupTarget.classList.toggle(this.activeClass)
66 | }
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/assets/src/controllers/subscription_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 |
3 | export default class extends Controller {
4 | static values = { price: String, stripe: String }
5 |
6 | connect(){
7 | this.stripe = Stripe(this.stripeValue);
8 | }
9 |
10 | showErrorMessage(message) {
11 | let errorEl = document.getElementById("error-message")
12 | errorEl.textContent = message;
13 | errorEl.style.display = "block";
14 | };
15 |
16 | handleFetchResult(result) {
17 | const self = this;
18 | if (!result.ok) {
19 | return result.json().then(function(json) {
20 | if (json.error && json.error.message) {
21 | throw new Error(result.url + ' ' + result.status + ' ' + json.error.message);
22 | }
23 | }).catch(function(err) {
24 | self.showErrorMessage(err);
25 | throw err;
26 | });
27 | }
28 | return result.json();
29 | };
30 |
31 | handleResult(result) {
32 | if (result.error) {
33 | this.showErrorMessage(result.error.message);
34 | }
35 | };
36 |
37 | createCheckoutSession(priceId) {
38 | const self = this;
39 | return fetch("/account/checkout", {
40 | method: "POST",
41 | headers: {
42 | "Content-Type": "application/json"
43 | },
44 | body: JSON.stringify({
45 | price: priceId
46 | })
47 | }).then(self.handleFetchResult);
48 | };
49 |
50 |
51 | createCheckoutClick(){
52 | const self = this;
53 | this.createCheckoutSession(this.priceValue).then(function(data) {
54 | // Call Stripe.js method to redirect to the new Checkout page
55 | self.stripe
56 | .redirectToCheckout({
57 | sessionId: data.sessionId
58 | })
59 | .then(self.handleResult);
60 | });
61 | //console.log("values => ",this.priceValue, this.stripeValue)
62 | }
63 | }
--------------------------------------------------------------------------------
/assets/src/controllers/svelte_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus";
2 | import components from "../components";
3 |
4 | export default class extends Controller {
5 | static targets = ["component"]
6 | connect() {
7 | if (this.componentTargets.length > 0){
8 | this.componentTargets.forEach(el => {
9 | const componentName = el.dataset.componentName;
10 | const componentProps = el.dataset.componentProps ? JSON.parse(el.dataset.componentProps): {};
11 | if (!(componentName in components)){
12 | console.log(`svelte component: ${componentName}, not found!`)
13 | return;
14 | }
15 | const app = new components[componentName]({
16 | target: el,
17 | props: componentProps
18 | });
19 | })
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/assets/src/controllers/tabs_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 |
3 | export default class extends Controller {
4 |
5 | static targets = [ "tab", "tabPanel" ]
6 | static values = {
7 | defaultTabkey: String,
8 | disableHistory: Boolean
9 | }
10 | static classes = [ "active", "hidden" ]
11 |
12 | connect() {
13 | let activeTabKey = undefined;
14 | if (window.location.hash && !this.disableHistoryValue){
15 | activeTabKey = window.location.hash.substring(1);
16 | }else if (!this.disableHistoryValue){
17 | activeTabKey = localStorage.getItem('tabkey')
18 | }
19 | if (!activeTabKey){
20 | activeTabKey = this.defaultTabkeyValue;
21 | }
22 |
23 | this.activateTab(activeTabKey)
24 | }
25 |
26 | activateTab(tabkey){
27 | this.tabPanelTargets.forEach((el, i) => {
28 | if(el.dataset.tabkey === tabkey){
29 | el.classList.remove(this.hiddenClass)
30 | } else {
31 | el.classList.add(this.hiddenClass)
32 | }
33 | })
34 |
35 | this.tabTargets.forEach((el, i) => {
36 | if(el.dataset.tabkey === tabkey){
37 | el.classList.add(this.activeClass)
38 | if (!this.disableHistoryValue){
39 | history.replaceState(null,null,`#${el.dataset.tabkey}`)
40 | localStorage.setItem("tabkey",el.dataset.tabkey)
41 | }
42 | } else {
43 | el.classList.remove(this.activeClass)
44 | }
45 | })
46 | }
47 |
48 | activate(e){
49 | let tabkey = undefined;
50 | if(e.currentTarget.dataset.tabkey){
51 | tabkey = e.currentTarget.dataset.tabkey;
52 | } else if(e.currentTarget.parentElement.dataset.tabkey){
53 | tabkey = e.currentTarget.parentElement.dataset.tabkey
54 | }
55 | this.activateTab(tabkey);
56 | }
57 |
58 | }
--------------------------------------------------------------------------------
/assets/src/controllers/toggle_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 |
3 | export default class extends Controller {
4 | static targets = [ "toggled" ]
5 | static values = {
6 | toggleClass: String
7 | }
8 |
9 | it(e){
10 | e.preventDefault()
11 | this.toggledTargets.forEach(item => {
12 | console.log("item")
13 | item.classList.toggle(this.toggleClassValue)
14 | })
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/assets/src/index.js:
--------------------------------------------------------------------------------
1 | import { Application } from "stimulus"
2 | import { definitionsFromContext } from "stimulus/webpack-helpers"
3 | import * as Turbo from "@hotwired/turbo"
4 | import "./styles.scss";
5 | const application = Application.start()
6 | const context = require.context("./controllers", true, /\.js$/)
7 | application.load(definitionsFromContext(context))
8 |
--------------------------------------------------------------------------------
/assets/src/styles.scss:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 |
4 | .h-full {
5 | min-height: 101vh !important;
6 | }
7 |
8 | .width-320 {
9 | width: 320px !important;
10 | }
11 |
12 | //$navbar-background-color: #002C73;
13 | //$primary: #002C73;
14 |
15 | @import "~bulma/bulma";
16 | .is-clickable:hover {
17 | cursor: pointer;
18 | }
19 |
20 |
21 |
22 |
23 | // https://bulma.io/documentation/customize/with-webpack/#9-add-your-own-bulma-styles
--------------------------------------------------------------------------------
/assets/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const glob = require('glob-all')
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
4 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
5 | const PurgeCssPlugin = require('purgecss-webpack-plugin')
6 | const CopyPlugin = require('copy-webpack-plugin');
7 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
8 |
9 | const PATHS = {
10 | html: path.join(__dirname, '../templates'),
11 | src: path.join(__dirname, 'src')
12 | }
13 |
14 | const env = process.env.NODE_ENV;
15 |
16 |
17 | module.exports = {
18 | mode: env === 'production' || env === 'none' ? env : 'development',
19 | entry: './src/index.js',
20 | resolve: {
21 | alias: {
22 | svelte: path.resolve('node_modules', 'svelte')
23 | },
24 | extensions: ['.mjs', '.js', '.svelte'],
25 | mainFields: ['svelte', 'browser', 'module', 'main']
26 | },
27 | output: {
28 | path: path.resolve(__dirname, '../public'),
29 | filename: 'assets/js/bundle.js'
30 | },
31 | module: {
32 | rules: [
33 | {
34 | test: /\.svelte$/,
35 | use: {
36 | loader: 'svelte-loader',
37 | options: {
38 | emitCss: true,
39 | hotReload: true
40 | }
41 | }
42 | },
43 | {
44 | test: /\.js$/,
45 | exclude: [
46 | /node_modules/
47 | ],
48 | use: [
49 | {loader: "babel-loader"}
50 | ]
51 | },
52 | {
53 | test: /\.css$/,
54 | use: [
55 | /**
56 | * MiniCssExtractPlugin doesn't support HMR.
57 | * For developing, use 'style-loader' instead.
58 | * */
59 | env === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',
60 | 'css-loader'
61 | ]
62 | },
63 | {
64 | test: /\.scss$/,
65 | use: [
66 | MiniCssExtractPlugin.loader,
67 | {
68 | loader: 'css-loader'
69 | },
70 | {
71 | loader: 'sass-loader',
72 | options: {
73 | sourceMap: true,
74 | // options...
75 | }
76 | }
77 | ]
78 | }]
79 | },
80 | plugins: [
81 | new MiniCssExtractPlugin({
82 | filename: 'assets/css/styles.css'
83 | }),
84 | new CopyPlugin({
85 | patterns: [
86 | { from: 'images', to: 'assets/images' },
87 | ],
88 | }),
89 | // new BundleAnalyzerPlugin(),
90 | ]
91 | };
92 |
93 | if (env === 'production') {
94 | module.exports.plugins.push(
95 | new OptimizeCssAssetsPlugin({
96 | cssProcessorPluginOptions: {
97 | preset: ['default', { discardComments: { removeAll: true } }]
98 | }
99 | })
100 | );
101 |
102 | module.exports.plugins.push(
103 | new PurgeCssPlugin({
104 | paths: glob.sync([
105 | `${PATHS.html}/**/*`,
106 | `${PATHS.src}/**/*`],
107 | {nodir: true}),
108 | }),
109 | );
110 | }
--------------------------------------------------------------------------------
/env.development:
--------------------------------------------------------------------------------
1 | APP_DOMAIN=http://localhost:4000
2 | APP_LOG_LEVEL=error
3 | APP_GOOGLE_CLIENT_ID=
4 | APP_GOOGLE_SECRET=
5 | APP_SMTP_HOST=
6 | APP_SMTP_PORT=587
7 | APP_SMTP_USER=
8 | APP_SMTP_PASS=
9 | APP_SMTP_ADMIN_EMAIL=
10 | APP_API_MASTER_SECRET=
11 | APP_STRIPE_PUBLISHABLE_KEY=
12 | APP_STRIPE_SECRET_KEY=
13 | APP_STRIPE_WEBHOOK_SECRET=
14 | APP_PLANS_FILE=
--------------------------------------------------------------------------------
/feature_groups.development.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Usage",
4 | "features": [
5 | {
6 | "id": "api_calls",
7 | "title": "API Calls",
8 | "value_type": "int"
9 | },
10 | {
11 | "id": "events",
12 | "title": "Events",
13 | "value_type": "int"
14 | },
15 | {
16 | "id": "alerts",
17 | "title": "Alerts",
18 | "value_type": "int"
19 | }
20 | ]
21 | }
22 | ]
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/adnaan/gomodest-starter
2 |
3 | go 1.16
4 |
5 | require (
6 | cloud.google.com/go v0.74.0 // indirect
7 | entgo.io/ent v0.7.0
8 | github.com/PuerkitoBio/goquery v1.6.0 // indirect
9 | github.com/adnaan/authn v0.0.2
10 | github.com/adnaan/renderlayout v0.0.3
11 | github.com/andybalholm/cascadia v1.2.0 // indirect
12 | github.com/go-chi/chi v4.1.2+incompatible
13 | github.com/go-chi/httplog v0.1.8
14 | github.com/go-chi/render v1.0.1
15 | github.com/go-chi/valve v0.0.0-20170920024740-9e45288364f4
16 | github.com/go-playground/form v3.1.4+incompatible
17 | github.com/golang/protobuf v1.5.1 // indirect
18 | github.com/google/uuid v1.2.0
19 | github.com/gorilla/mux v1.8.0 // indirect
20 | github.com/hako/branca v0.0.0-20200807062402-6052ac720505
21 | github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 // indirect
22 | github.com/joho/godotenv v1.3.0
23 | github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
24 | github.com/kelseyhightower/envconfig v1.4.0
25 | github.com/lithammer/shortuuid/v3 v3.0.5
26 | github.com/markbates/goth v1.67.1
27 | github.com/matcornic/hermes/v2 v2.1.0
28 | github.com/mattn/go-sqlite3 v1.14.6
29 | github.com/mholt/binding v0.3.0
30 | github.com/rs/zerolog v1.20.0 // indirect
31 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
32 | github.com/stripe/stripe-go/v72 v72.28.0
33 | github.com/vanng822/go-premailer v1.9.0 // indirect
34 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
35 | golang.org/x/mod v0.4.1 // indirect
36 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 // indirect
37 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84 // indirect
38 | )
39 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "os"
10 | "os/signal"
11 | "path/filepath"
12 | "strings"
13 | "time"
14 |
15 | "github.com/adnaan/gomodest-starter/app"
16 |
17 | "github.com/go-chi/chi"
18 |
19 | "github.com/go-chi/valve"
20 | )
21 |
22 | // fileServer conveniently sets up a http.FileServer handler to serve
23 | // static files from a http.FileSystem.
24 | func fileServer(r chi.Router, path string, root http.FileSystem) {
25 | if strings.ContainsAny(path, "{}*") {
26 | panic("FileServer does not permit any URL parameters.")
27 | }
28 |
29 | if path != "/" && path[len(path)-1] != '/' {
30 | r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
31 | path += "/"
32 | }
33 | path += "*"
34 |
35 | r.Get(path, func(w http.ResponseWriter, r *http.Request) {
36 | rctx := chi.RouteContext(r.Context())
37 | pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
38 | fs := http.StripPrefix(pathPrefix, http.FileServer(root))
39 | fs.ServeHTTP(w, r)
40 | })
41 | }
42 |
43 | func main() {
44 | // Our graceful valve shut-off package to manage code preemption and
45 | // shutdown signaling.
46 | vv := valve.New()
47 | baseCtx := vv.Context()
48 | configFile := flag.String("config", "", "path to config file")
49 | envPrefix := os.Getenv("ENV_PREFIX")
50 | if envPrefix == "" {
51 | envPrefix = "app"
52 | }
53 | flag.Parse()
54 |
55 | cfg, err := app.LoadConfig(*configFile, envPrefix)
56 | if err != nil {
57 | log.Fatal(err)
58 | }
59 |
60 | r := app.Router(baseCtx, cfg)
61 | workDir, _ := os.Getwd()
62 | public := http.Dir(filepath.Join(workDir, "./", "public", "assets"))
63 | fileServer(r, "/static", public)
64 |
65 | srv := &http.Server{
66 | ReadTimeout: time.Duration(cfg.ReadTimeoutSecs) * time.Second,
67 | WriteTimeout: time.Duration(cfg.WriteTimeoutSecs) * time.Second,
68 | Addr: fmt.Sprintf(":%d", cfg.Port),
69 | Handler: r,
70 | }
71 |
72 | c := make(chan os.Signal, 1)
73 | signal.Notify(c, os.Interrupt)
74 | go func() {
75 | for range c {
76 | // sig is a ^C, handle it
77 | fmt.Println("shutting down..")
78 |
79 | // first valv
80 | vv.Shutdown(20 * time.Second)
81 |
82 | // create context with timeout
83 | ctx, cancel := context.WithTimeout(baseCtx, 20*time.Second)
84 | defer cancel()
85 |
86 | // start http shutdown
87 | srv.Shutdown(ctx)
88 |
89 | // verify, in worst case call cancel via defer
90 | select {
91 | case <-time.After(21 * time.Second):
92 | fmt.Println("not all connections done")
93 | case <-ctx.Done():
94 |
95 | }
96 | }
97 | }()
98 | log.Println("http server is listening...")
99 | srv.ListenAndServe()
100 | }
101 |
--------------------------------------------------------------------------------
/plans.development.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "price_id": "free",
4 | "name": "Free",
5 | "price": "0",
6 | "details": [
7 | "1000 API Calls",
8 | "10K Events",
9 | "100 Alerts"
10 | ]
11 | },
12 | {
13 | "price_id": "pro",
14 | "name": "Pro",
15 | "price": "10",
16 | "details": [
17 | "10000 API Calls",
18 | "100K Events",
19 | "1000 Alerts"
20 | ]
21 | }
22 | ]
--------------------------------------------------------------------------------
/stripe.md:
--------------------------------------------------------------------------------
1 | ```bash
2 | stripe products create --name="Free" --description="Free plan"
3 | stripe prices create \
4 | -d product=prod_IZQ0fAMigCUFX1 \
5 | -d unit_amount=0 \
6 | -d currency=eur \
7 | -d "recurring[interval]"=month
8 | stripe products create --name="Pro" --description="Pro plan"
9 | stripe prices create \
10 | -d product=prod_IZQ2RckEQabB3y \
11 | -d unit_amount=10 \
12 | -d currency=eur \
13 | -d "recurring[interval]"=month
14 | ```
--------------------------------------------------------------------------------
/templates/404.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
5 |
404 Page not found
6 |
7 |
Home
10 |
11 |
12 | {{end}}
--------------------------------------------------------------------------------
/templates/account/changed.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
Confirm email change.
5 |
6 |
7 |
8 | {{end}}
--------------------------------------------------------------------------------
/templates/account/confirmed.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 | Email confirmed
5 |
6 |
7 |
8 |
9 | {{end}}
--------------------------------------------------------------------------------
/templates/account/forgot.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
5 | {{template "errors" .}}
6 | {{ if .recovery_sent }}
7 | A password recovery link has been sent to your inbox.
8 | {{else}}
9 |
10 |
34 | {{end}}
35 |
36 |
37 |
38 | {{end}}
--------------------------------------------------------------------------------
/templates/account/login.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
5 | {{template "errors" .}}
6 | {{ if .confirmed }}
7 |
8 | Thank you for confirming your email. Sign in to get started !
9 |
10 | {{end}}
11 |
12 | {{ if .not_confirmed }}
13 |
14 | Email not confirmed yet ! An email was sent to your inbox for confirmation.
15 | Please confirm to login.
16 |
17 | {{end}}
18 |
19 | {{ if .confirmation_sent }}
20 |
21 | An email has been sent to your inbox for confirmation. Please confirm to login.
22 |
23 | {{end}}
24 |
25 |
26 |
Sign in to {{.app_name}}
27 |
28 |
29 |
31 |
32 |
33 |
37 | Use Password instead
38 |
39 |
40 |
71 |
72 |
73 |
74 |
75 | Use Magic Link instead
78 |
79 |
80 |
130 |
131 |
132 |
133 |
134 |
138 |
139 |
140 |
141 | Sign in with Google
142 |
143 |
144 |
145 |
146 |
147 | {{end}}
--------------------------------------------------------------------------------
/templates/account/magic.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
5 |
6 | We have sent a one time login link to your email inbox.
7 | Simply click on the link to login.
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{end}}
--------------------------------------------------------------------------------
/templates/account/main.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 | {{ if .change_email }}
5 |
A verification link has been sent to your inbox for confirming your new
6 | email address. Please verify to complete the request.
7 |
8 | {{end}}
9 |
10 | {{ if .email_changed }}
11 |
Your email was updated!
12 | {{end}}
13 |
14 | {{ if .checkout }}
15 | {{ if eq .checkout "success"}}
16 |
Payment was successful!
17 | {{end}}
18 | {{ if eq .checkout "cancel"}}
19 |
Payment was cancelled!
20 | {{end}}
21 | {{end}}
22 |
23 |
24 |
29 |
30 |
68 |
69 |
70 |
74 | {{template "profile" .}}
75 |
76 |
80 | {{template "plans" .}}
81 |
82 |
86 | {{template "developer" .}}
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {{end}}
--------------------------------------------------------------------------------
/templates/account/main_workspace.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
5 | {{ if .change_email }}
6 |
A verification link has been sent to your inbox for confirming your new
7 | email address. Please verify to complete the request.
8 |
9 | {{end}}
10 |
11 | {{ if .email_changed }}
12 |
Your email was updated!
13 | {{end}}
14 |
15 | {{ if .checkout }}
16 | {{ if eq .checkout "success"}}
17 |
Payment was successful!
18 | {{end}}
19 | {{ if eq .checkout "cancel"}}
20 |
Payment was cancelled!
21 | {{end}}
22 | {{end}}
23 |
24 | {{ template "workspace_switcher" .}}
25 |
26 |
31 |
32 |
77 |
78 |
79 |
83 | {{template "profile" .}}
84 |
85 |
89 | {{template "plans" .}}
90 |
91 |
95 | {{template "developer" .}}
96 |
97 |
101 | {{template "members" .}}
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | {{end}}
--------------------------------------------------------------------------------
/templates/account/reset.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
5 | {{template "errors" .}}
6 | Change to a new pasword
7 |
8 |
9 |
43 |
44 |
45 |
46 | {{end}}
--------------------------------------------------------------------------------
/templates/account/signup.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
5 | {{template "errors" .}}
6 |
7 |
Sign up with {{.app_name}}
8 |
9 |
76 |
77 |
78 |
79 | {{end}}
--------------------------------------------------------------------------------
/templates/app.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
5 | {{template "errors" .}}
6 |
24 |
25 | {{ range .tasks }}
26 |
27 |
28 |
29 |
30 |
31 | {{.Text}}
32 |
33 |
34 |
35 |
38 |
42 |
43 |
44 |
45 |
46 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
86 |
87 |
112 | {{ end }}
113 |
114 |
115 |
116 |
117 | {{end}}
--------------------------------------------------------------------------------
/templates/home.html:
--------------------------------------------------------------------------------
1 | {{define "content"}}
2 |
3 |
4 |
{{.app_name}} is inspired from modestjs approaches to building webapps as enlisted in https://modestjs.works/.
5 |
6 | Using Go, SveltJS, StimulusJS and Bulma.css
7 |
8 |
9 |
10 |
11 | {{end}}
--------------------------------------------------------------------------------
/templates/layouts/empty.html:
--------------------------------------------------------------------------------
1 | {{template "content" .}}
--------------------------------------------------------------------------------
/templates/layouts/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{.app_name}}
6 | {{include "partials/header"}}
7 |
8 |
11 | {{include "partials/navbar"}}
12 |
13 |
14 |
15 |
16 |
Form validation failed
17 |
18 |
19 |
20 | {{template "content" .}}
21 |
22 | {{include "partials/footer"}}
23 |
24 |
25 |
--------------------------------------------------------------------------------
/templates/partials/developer.html:
--------------------------------------------------------------------------------
1 | {{define "developer"}}
2 |
3 |
Api Token
4 |
5 | {{ if .is_api_token_set }}
6 | {{ if .api_token }}
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Copied to Clipboard!
17 |
18 |
19 |
20 |
21 |
22 | Please copy the API Token and save it in a safe place.
23 | You won't be able to see the token again once this page is closed or reloaded.
24 |
25 |
26 |
27 | {{ else }}
28 |
You have previously generated an API Token and hopefully saved it in a safe place.
29 |
30 |
33 | Regenerate API Token
34 |
35 | {{ end }}
36 | {{ else }}
37 |
40 | Generate API Token
41 |
42 | {{ end }}
43 |
44 |
78 |
79 | {{end}}
--------------------------------------------------------------------------------
/templates/partials/errors.html:
--------------------------------------------------------------------------------
1 | {{define "errors"}}
2 | {{if .errors}}
3 |
4 | {{ range .errors }}
5 |
{{.}}
6 | {{end}}
7 |
8 | {{end}}
9 | {{end}}
--------------------------------------------------------------------------------
/templates/partials/footer.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/partials/groups.html:
--------------------------------------------------------------------------------
1 | {{define "groups"}}
2 |
6 |
7 | {{end}}
--------------------------------------------------------------------------------
/templates/partials/guests.html:
--------------------------------------------------------------------------------
1 | {{define "guests"}}
2 |
5 |
6 | {{end}}
--------------------------------------------------------------------------------
/templates/partials/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/templates/partials/list_members.html:
--------------------------------------------------------------------------------
1 | {{define "list_members"}}
2 |
6 |
7 | {{end}}
--------------------------------------------------------------------------------
/templates/partials/members.html:
--------------------------------------------------------------------------------
1 | {{define "members"}}
2 |
8 |
24 |
25 |
28 | {{template "list_members" .}}
29 |
30 |
33 | {{template "groups" .}}
34 |
35 |
38 | {{template "guests" .}}
39 |
40 |
41 |
42 | {{end}}
--------------------------------------------------------------------------------
/templates/partials/navbar.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
48 |
49 |
--------------------------------------------------------------------------------
/templates/partials/plan.html:
--------------------------------------------------------------------------------
1 | {{define "plan"}}
2 |
3 |
4 |
5 |
6 |
Name
7 |
{{.Name}}
8 |
9 |
10 |
11 |
12 |
13 |
$/Month
14 |
{{.Price}}
15 |
16 |
17 |
18 |
21 | View Details
22 |
23 |
26 |
27 |
28 |
29 | {{.Name}} Plan
30 |
34 |
35 |
36 |
37 |
38 | {{range .Details}}
39 |
40 | {{.}}
41 |
42 | {{end}}
43 |
44 |
45 |
46 |
53 |
54 |
55 |
56 |
60 |
61 | {{ if eq .PriceID "default" }}
62 |
Current plan
63 | {{ else }}
64 | {{ if not .Current }}
65 |
67 | Upgrade
68 |
69 | {{else}}
70 |
73 | Manage
74 |
75 | {{end}}
76 | {{end}}
77 |
80 |
81 |
82 |
83 | Are you sure ?
84 |
88 |
89 |
90 |
91 |
92 | Changing your current plan will change the invoice immediately.
93 |
94 |
95 |
110 |
111 |
112 |
113 |
114 |
115 | {{end}}
--------------------------------------------------------------------------------
/templates/partials/plans.html:
--------------------------------------------------------------------------------
1 | {{define "plans"}}
2 |
3 |
Plans
4 |
5 |
6 |
7 |
8 |
9 | {{ $currentPlan := .current_plan }}
10 |
11 | {{ range .plans}}
12 |
16 | {{.Name}}
17 | ${{.Price}} per month
18 |
19 | {{ if $currentPlan }}
20 | {{ if eq $currentPlan.PriceID .PriceID }}
21 |
22 |
23 | Current plan
24 |
25 |
26 |
27 |
30 | Manage
31 |
32 |
33 | {{ end }}
34 | {{ else }}
35 | {{ if eq .PriceID "default" }}
36 |
37 | Current plan
38 |
39 | {{ else }}
40 |
42 | Upgrade
43 |
44 | {{ end }}
45 | {{ end }}
46 |
47 |
48 | {{ end }}
49 |
50 |
51 |
52 | {{ $plans := .plans }}
53 | {{ range .feature_groups }}
54 |
55 | {{ .Name }}
56 | {{ range $plans }}
57 |
58 | {{ end }}
59 |
60 |
61 | {{ range .Features }}
62 | {{$featureID := .ID}}
63 |
64 | {{.Title}}
65 | {{ range $plans }}
66 | {{ index .Features $featureID}}
67 | {{ end }}
68 |
69 | {{ end }}
70 |
71 | {{ end }}
72 |
73 |
74 |
75 |
76 | {{end}}
--------------------------------------------------------------------------------
/templates/partials/profile.html:
--------------------------------------------------------------------------------
1 | {{define "profile"}}
2 |
55 |
56 |
57 |
Delete account
58 |
59 |
All data related to your account will be completely removed and unrecoverable.
60 |
63 | Delete your account
64 |
65 |
96 |
97 | {{end}}
--------------------------------------------------------------------------------
/templates/partials/workspace_switcher.html:
--------------------------------------------------------------------------------
1 | {{define "workspace_switcher"}}
2 |
3 |
4 |
35 |
36 |
37 |
38 |
39 | {{end}}
--------------------------------------------------------------------------------