Skip to content

Commit 7f34d4d

Browse files
authored
refactor: decouple controllers from models, reduce number of DB queries (#934)
1 parent 1093cc3 commit 7f34d4d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1688
-1715
lines changed

api/docs.go

Lines changed: 228 additions & 184 deletions
Large diffs are not rendered by default.

api/swagger.json

Lines changed: 228 additions & 184 deletions
Large diffs are not rendered by default.

api/swagger.yaml

Lines changed: 173 additions & 145 deletions
Large diffs are not rendered by default.

pkg/controllers/v3/account.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ func OptionsAccountDetail(c *gin.Context) {
5656
return
5757
}
5858

59-
var account models.Account
60-
err = query(c, models.DB.First(&account, id))
59+
_, err = getModelByID[models.Account](c, id)
6160
if !err.Nil() {
6261
c.JSON(err.Status, httperrors.HTTPError{
6362
Error: err.Error(),
@@ -99,7 +98,7 @@ func CreateAccounts(c *gin.Context) {
9998

10099
// Verify that budget exists. If not, append the error
101100
// and move to the next account
102-
_, err := getResourceByID[models.Budget](c, editable.BudgetID)
101+
_, err := getModelByID[models.Budget](c, editable.BudgetID)
103102
if !err.Nil() {
104103
status = r.appendError(err, status)
105104
continue
@@ -243,12 +242,10 @@ func GetAccount(c *gin.Context) {
243242
return
244243
}
245244

246-
var account models.Account
247-
err = query(c, models.DB.First(&account, id))
245+
account, err := getModelByID[models.Account](c, id)
248246
if !err.Nil() {
249-
s := err.Error()
250-
c.JSON(err.Status, AccountResponse{
251-
Error: &s,
247+
c.JSON(err.Status, httperrors.HTTPError{
248+
Error: err.Error(),
252249
})
253250
return
254251
}
@@ -286,7 +283,7 @@ func UpdateAccount(c *gin.Context) {
286283
return
287284
}
288285

289-
account, err := getResourceByID[models.Account](c, id)
286+
account, err := getModelByID[models.Account](c, id)
290287
if !err.Nil() {
291288
s := err.Error()
292289
c.JSON(err.Status, AccountResponse{
@@ -354,7 +351,7 @@ func DeleteAccount(c *gin.Context) {
354351
return
355352
}
356353

357-
account, err := getResourceByID[models.Account](c, id)
354+
account, err := getModelByID[models.Account](c, id)
358355
if !err.Nil() {
359356
c.JSON(err.Status, httperrors.HTTPError{
360357
Error: err.Error(),

pkg/controllers/v3/account_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
func (suite *TestSuiteStandard) createTestAccount(t *testing.T, account models.Account, expectedStatus ...int) v3.AccountResponse {
1919
if account.BudgetID == uuid.Nil {
20-
account.BudgetID = suite.createTestBudget(t, models.BudgetCreate{Name: "Testing budget"}).Data.ID
20+
account.BudgetID = suite.createTestBudget(t, v3.BudgetEditable{Name: "Testing budget"}).Data.ID
2121
}
2222

2323
body := []models.Account{
@@ -45,7 +45,7 @@ func (suite *TestSuiteStandard) createTestAccount(t *testing.T, account models.A
4545
// TestAccountsDBClosed verifies that errors are processed correctly when
4646
// the database is closed.
4747
func (suite *TestSuiteStandard) TestAccountsDBClosed() {
48-
b := suite.createTestBudget(suite.T(), models.BudgetCreate{})
48+
b := suite.createTestBudget(suite.T(), v3.BudgetEditable{})
4949

5050
tests := []struct {
5151
name string // Name of the test
@@ -137,8 +137,8 @@ func (suite *TestSuiteStandard) TestAccountsGetSingle() {
137137
}
138138

139139
func (suite *TestSuiteStandard) TestAccountsGetFilter() {
140-
b1 := suite.createTestBudget(suite.T(), models.BudgetCreate{})
141-
b2 := suite.createTestBudget(suite.T(), models.BudgetCreate{})
140+
b1 := suite.createTestBudget(suite.T(), v3.BudgetEditable{})
141+
b2 := suite.createTestBudget(suite.T(), v3.BudgetEditable{})
142142

143143
_ = suite.createTestAccount(suite.T(), models.Account{
144144
Name: "Exact Account Match",
@@ -306,7 +306,7 @@ func (suite *TestSuiteStandard) TestAccountsCreateFails() {
306306

307307
// Verify that updating accounts works as desired
308308
func (suite *TestSuiteStandard) TestAccountsUpdate() {
309-
budget := suite.createTestBudget(suite.T(), models.BudgetCreate{})
309+
budget := suite.createTestBudget(suite.T(), v3.BudgetEditable{})
310310
account := suite.createTestAccount(suite.T(), models.Account{Name: "Original name", BudgetID: budget.Data.ID})
311311

312312
tests := []struct {

pkg/controllers/v3/account_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Account struct {
5252
AccountEditable
5353
Links AccountLinks `json:"links"`
5454

55-
// These fields are calculated
55+
// These fields are computed
5656
Balance decimal.Decimal `json:"balance" example:"2735.17"` // Balance of the account, including all transactions referencing it
5757
ReconciledBalance decimal.Decimal `json:"reconciledBalance" example:"2539.57"` // Balance of the account, including all reconciled transactions referencing it
5858
RecentEnvelopes []*uuid.UUID `json:"recentEnvelopes"` // Envelopes recently used with this account

pkg/controllers/v3/budget.go

Lines changed: 26 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,15 @@
11
package v3
22

33
import (
4-
"fmt"
54
"net/http"
65

76
"github.com/envelope-zero/backend/v4/pkg/httperrors"
87
"github.com/envelope-zero/backend/v4/pkg/httputil"
98
"github.com/envelope-zero/backend/v4/pkg/models"
109
"github.com/gin-gonic/gin"
11-
"github.com/google/uuid"
1210
"golang.org/x/exp/slices"
1311
)
1412

15-
type BudgetQueryFilter struct {
16-
Name string `form:"name" filterField:"false"` // By name
17-
Note string `form:"note" filterField:"false"` // By note
18-
Currency string `form:"currency"` // By currency
19-
Search string `form:"search" filterField:"false"` // By string in name or note
20-
Offset uint `form:"offset" filterField:"false"` // The offset of the first Budget returned. Defaults to 0.
21-
Limit int `form:"limit" filterField:"false"` // Maximum number of Budgets to return. Defaults to 50.
22-
}
23-
24-
// Budget is the API v3 representation of a Budget.
25-
type Budget struct {
26-
models.Budget
27-
Links struct {
28-
Self string `json:"self" example:"https://example.com/api/v3/budgets/550dc009-cea6-4c12-b2a5-03446eb7b7cf"` // The budget itself
29-
Accounts string `json:"accounts" example:"https://example.com/api/v3/accounts?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf"` // Accounts for this budget
30-
Categories string `json:"categories" example:"https://example.com/api/v3/categories?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf"` // Categories for this budget
31-
Envelopes string `json:"envelopes" example:"https://example.com/api/v3/envelopes?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf"` // Envelopes for this budget
32-
Transactions string `json:"transactions" example:"https://example.com/api/v3/transactions?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf"` // Transactions for this budget
33-
Month string `json:"month" example:"https://example.com/api/v3/months?budget=550dc009-cea6-4c12-b2a5-03446eb7b7cf&month=YYYY-MM"` // This uses 'YYYY-MM' for clients to replace with the actual year and month.
34-
} `json:"links"`
35-
}
36-
37-
// links sets all links for the Budget.
38-
func (b *Budget) links(c *gin.Context) {
39-
url := c.GetString(string(models.DBContextURL))
40-
41-
b.Links.Self = fmt.Sprintf("%s/v3/budgets/%s", url, b.ID)
42-
b.Links.Accounts = fmt.Sprintf("%s/v3/accounts?budget=%s", url, b.ID)
43-
b.Links.Categories = fmt.Sprintf("%s/v3/categories?budget=%s", url, b.ID)
44-
b.Links.Envelopes = fmt.Sprintf("%s/v3/envelopes?budget=%s", url, b.ID)
45-
b.Links.Transactions = fmt.Sprintf("%s/v3/transactions?budget=%s", url, b.ID)
46-
b.Links.Month = fmt.Sprintf("%s/v3/months?budget=%s&month=YYYY-MM", url, b.ID)
47-
}
48-
49-
// getBudget returns a budget with all fields set.
50-
func getBudget(c *gin.Context, id uuid.UUID) (Budget, httperrors.Error) {
51-
m, err := getResourceByID[models.Budget](c, id)
52-
if !err.Nil() {
53-
return Budget{}, err
54-
}
55-
56-
b := Budget{
57-
Budget: m,
58-
}
59-
60-
b.links(c)
61-
62-
return b, httperrors.Error{}
63-
}
64-
65-
type BudgetListResponse struct {
66-
Data []Budget `json:"data"` // List of budgets
67-
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred
68-
Pagination *Pagination `json:"pagination"` // Pagination information
69-
}
70-
71-
type BudgetCreateResponse struct {
72-
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred
73-
Data []BudgetResponse `json:"data"` // List of created Budgets
74-
}
75-
76-
func (b *BudgetCreateResponse) appendError(err httperrors.Error, status int) int {
77-
s := err.Error()
78-
b.Data = append(b.Data, BudgetResponse{Error: &s})
79-
80-
// The final status code is the highest HTTP status code number
81-
if err.Status > status {
82-
status = err.Status
83-
}
84-
85-
return status
86-
}
87-
88-
type BudgetResponse struct {
89-
Data *Budget `json:"data"` // Data for the budget
90-
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred
91-
}
92-
9313
// RegisterBudgetRoutes registers the routes for Budgets with
9414
// the RouterGroup that is passed.
9515
func RegisterBudgetRoutes(r *gin.RouterGroup) {
@@ -136,7 +56,7 @@ func OptionsBudgetDetail(c *gin.Context) {
13656
return
13757
}
13858

139-
_, err = getResourceByID[models.Budget](c, id)
59+
_, err = getModelByID[models.Budget](c, id)
14060
if !err.Nil() {
14161
c.JSON(err.Status, httperrors.HTTPError{
14262
Error: err.Error(),
@@ -155,10 +75,10 @@ func OptionsBudgetDetail(c *gin.Context) {
15575
// @Success 201 {object} BudgetCreateResponse
15676
// @Failure 400 {object} BudgetCreateResponse
15777
// @Failure 500 {object} BudgetCreateResponse
158-
// @Param budget body models.BudgetCreate true "Budget"
78+
// @Param budget body []BudgetEditable true "Budget"
15979
// @Router /v3/budgets [post]
16080
func CreateBudgets(c *gin.Context) {
161-
var budgets []models.BudgetCreate
81+
var budgets []BudgetEditable
16282

16383
// Bind data and return error if not possible
16484
err := httputil.BindData(c, &budgets)
@@ -174,25 +94,18 @@ func CreateBudgets(c *gin.Context) {
17494
status := http.StatusCreated
17595
r := BudgetCreateResponse{}
17696

177-
for _, create := range budgets {
178-
b := models.Budget{
179-
BudgetCreate: create,
180-
}
97+
for _, editable := range budgets {
98+
budget := editable.model()
18199

182-
dbErr := models.DB.Create(&b).Error
100+
dbErr := models.DB.Create(&budget).Error
183101
if dbErr != nil {
184-
err := httperrors.GenericDBError[models.Budget](b, c, dbErr)
102+
err := httperrors.GenericDBError[models.Budget](budget, c, dbErr)
185103
status = r.appendError(err, status)
186104
continue
187105
}
188106

189-
// Append the budget
190-
bObject, err := getBudget(c, b.ID)
191-
if !err.Nil() {
192-
status = r.appendError(err, status)
193-
continue
194-
}
195-
r.Data = append(r.Data, BudgetResponse{Data: &bObject})
107+
data := newBudget(c, budget)
108+
r.Data = append(r.Data, BudgetResponse{Data: &data})
196109
}
197110

198111
c.JSON(status, r)
@@ -225,14 +138,7 @@ func GetBudgets(c *gin.Context) {
225138
// Always sort by name
226139
q := models.DB.
227140
Order("name ASC").
228-
Where(&models.Budget{
229-
BudgetCreate: models.BudgetCreate{
230-
Name: filter.Name,
231-
Note: filter.Note,
232-
Currency: filter.Currency,
233-
},
234-
},
235-
queryFields...)
141+
Where(filter.model(), queryFields...)
236142

237143
q = stringFilters(models.DB, q, setFields, filter.Name, filter.Note, filter.Search)
238144

@@ -265,23 +171,15 @@ func GetBudgets(c *gin.Context) {
265171
return
266172
}
267173

268-
budgetResources := make([]Budget, 0)
174+
apiResources := make([]Budget, 0)
269175
for _, budget := range budgets {
270-
r, err := getBudget(c, budget.ID)
271-
if !err.Nil() {
272-
s := err.Error()
273-
c.JSON(err.Status, BudgetListResponse{
274-
Error: &s,
275-
})
276-
return
277-
}
278-
budgetResources = append(budgetResources, r)
176+
apiResources = append(apiResources, newBudget(c, budget))
279177
}
280178

281179
c.JSON(http.StatusOK, BudgetListResponse{
282-
Data: budgetResources,
180+
Data: apiResources,
283181
Pagination: &Pagination{
284-
Count: len(budgetResources),
182+
Count: len(apiResources),
285183
Total: count,
286184
Offset: filter.Offset,
287185
Limit: limit,
@@ -309,16 +207,7 @@ func GetBudget(c *gin.Context) {
309207
return
310208
}
311209

312-
m, err := getResourceByID[models.Budget](c, id)
313-
if !err.Nil() {
314-
s := err.Error()
315-
c.JSON(err.Status, BudgetResponse{
316-
Error: &s,
317-
})
318-
return
319-
}
320-
321-
r, err := getBudget(c, m.ID)
210+
m, err := getModelByID[models.Budget](c, id)
322211
if !err.Nil() {
323212
s := err.Error()
324213
c.JSON(err.Status, BudgetResponse{
@@ -327,7 +216,8 @@ func GetBudget(c *gin.Context) {
327216
return
328217
}
329218

330-
c.JSON(http.StatusOK, BudgetResponse{Data: &r})
219+
apiResource := newBudget(c, m)
220+
c.JSON(http.StatusOK, BudgetResponse{Data: &apiResource})
331221
}
332222

333223
// @Summary Update budget
@@ -339,8 +229,8 @@ func GetBudget(c *gin.Context) {
339229
// @Failure 400 {object} BudgetResponse
340230
// @Failure 404 {object} BudgetResponse
341231
// @Failure 500 {object} BudgetResponse
342-
// @Param id path string true "ID formatted as string"
343-
// @Param budget body models.BudgetCreate true "Budget"
232+
// @Param id path string true "ID formatted as string"
233+
// @Param budget body BudgetEditable true "Budget"
344234
// @Router /v3/budgets/{id} [patch]
345235
func UpdateBudget(c *gin.Context) {
346236
id, err := httputil.UUIDFromString(c.Param("id"))
@@ -352,7 +242,7 @@ func UpdateBudget(c *gin.Context) {
352242
return
353243
}
354244

355-
budget, err := getResourceByID[models.Budget](c, id)
245+
budget, err := getModelByID[models.Budget](c, id)
356246
if !err.Nil() {
357247
s := err.Error()
358248
c.JSON(err.Status, BudgetResponse{
@@ -361,7 +251,7 @@ func UpdateBudget(c *gin.Context) {
361251
return
362252
}
363253

364-
updateFields, err := httputil.GetBodyFields(c, models.BudgetCreate{})
254+
updateFields, err := httputil.GetBodyFields(c, BudgetEditable{})
365255
if !err.Nil() {
366256
s := err.Error()
367257
c.JSON(err.Status, BudgetResponse{
@@ -370,8 +260,8 @@ func UpdateBudget(c *gin.Context) {
370260
return
371261
}
372262

373-
var data models.Budget
374-
err = httputil.BindData(c, &data.BudgetCreate)
263+
var data BudgetEditable
264+
err = httputil.BindData(c, &data)
375265
if !err.Nil() {
376266
s := err.Error()
377267
c.JSON(err.Status, BudgetResponse{
@@ -389,16 +279,8 @@ func UpdateBudget(c *gin.Context) {
389279
return
390280
}
391281

392-
r, err := getBudget(c, budget.ID)
393-
if !err.Nil() {
394-
s := err.Error()
395-
c.JSON(err.Status, BudgetResponse{
396-
Error: &s,
397-
})
398-
return
399-
}
400-
401-
c.JSON(http.StatusOK, BudgetResponse{Data: &r})
282+
apiResource := newBudget(c, budget)
283+
c.JSON(http.StatusOK, BudgetResponse{Data: &apiResource})
402284
}
403285

404286
// @Summary Delete budget
@@ -419,7 +301,7 @@ func DeleteBudget(c *gin.Context) {
419301
return
420302
}
421303

422-
budget, err := getResourceByID[models.Budget](c, id)
304+
budget, err := getModelByID[models.Budget](c, id)
423305
if !err.Nil() {
424306
c.JSON(err.Status, httperrors.HTTPError{
425307
Error: err.Error(),

0 commit comments

Comments
 (0)