Skip to content

Commit aa2dd96

Browse files
authored
feat: add export (#1003)
This adds functionality to export the whole state of the instance with an API call. The API call to /v4/export returns a JSON object with some meta information: * the version used to generate the export * the creation time With this change, go 1.22 is required since the tests now use range clauses over integers. This commit also creates a registry for models so that we do not need to re-implement iteration over all models in the places we need it (database migrations, cleanup, …).
1 parent 051aed3 commit aa2dd96

30 files changed

+694
-17
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,7 @@ repos:
5656
name: "Find FIXME:|BUG: comments"
5757
description: "Check for FIXME:|BUG: comments in all files"
5858
language: pygrep
59-
entry: '(^|//!?|#|<!--|;|/\*(\*|!)?|\.\.)\s*(FIXME:|BUG:)(?!#i#)'
59+
60+
# This contains "FIMXE" because I often mistype it
61+
entry: '(^|//!?|#|<!--|;|/\*(\*|!)?|\.\.)\s*(FIXME:|FIMXE:|BUG:)(?!#i#)'
6062
exclude: CONTRIBUTING.md

api/docs.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1665,6 +1665,44 @@ const docTemplate = `{
16651665
}
16661666
}
16671667
},
1668+
"/v4/export": {
1669+
"get": {
1670+
"description": "Exports all resources for the instance",
1671+
"produces": [
1672+
"application/json"
1673+
],
1674+
"tags": [
1675+
"Export"
1676+
],
1677+
"summary": "Export",
1678+
"responses": {
1679+
"200": {
1680+
"description": "OK",
1681+
"schema": {
1682+
"$ref": "#/definitions/v4.ExportResponse"
1683+
}
1684+
},
1685+
"500": {
1686+
"description": "Internal Server Error",
1687+
"schema": {
1688+
"$ref": "#/definitions/v4.ExportResponse"
1689+
}
1690+
}
1691+
}
1692+
},
1693+
"options": {
1694+
"description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs",
1695+
"tags": [
1696+
"Export"
1697+
],
1698+
"summary": "Allowed HTTP verbs",
1699+
"responses": {
1700+
"204": {
1701+
"description": "No Content"
1702+
}
1703+
}
1704+
}
1705+
},
16681706
"/v4/goals": {
16691707
"get": {
16701708
"description": "Returns a list of goals",
@@ -3460,7 +3498,7 @@ const docTemplate = `{
34603498
}
34613499
},
34623500
"error": {
3463-
"description": "FIMXE: make this *error for ALL responses",
3501+
"description": "The error, if any occurred",
34643502
"type": "string",
34653503
"example": "the specified resource ID is not a valid UUID"
34663504
}
@@ -4021,6 +4059,33 @@ const docTemplate = `{
40214059
}
40224060
}
40234061
},
4062+
"v4.ExportResponse": {
4063+
"type": "object",
4064+
"properties": {
4065+
"clacks": {
4066+
"description": "This will always have the value \"GNU Terry Pratchett\"",
4067+
"type": "string"
4068+
},
4069+
"creationTime": {
4070+
"description": "Time the export was created",
4071+
"type": "string"
4072+
},
4073+
"data": {
4074+
"description": "The exported data",
4075+
"type": "object",
4076+
"additionalProperties": {
4077+
"type": "array",
4078+
"items": {
4079+
"type": "integer"
4080+
}
4081+
}
4082+
},
4083+
"version": {
4084+
"description": "The version of the backend the export was made with",
4085+
"type": "string"
4086+
}
4087+
}
4088+
},
40244089
"v4.Goal": {
40254090
"type": "object",
40264091
"properties": {

api/swagger.json

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1654,6 +1654,44 @@
16541654
}
16551655
}
16561656
},
1657+
"/v4/export": {
1658+
"get": {
1659+
"description": "Exports all resources for the instance",
1660+
"produces": [
1661+
"application/json"
1662+
],
1663+
"tags": [
1664+
"Export"
1665+
],
1666+
"summary": "Export",
1667+
"responses": {
1668+
"200": {
1669+
"description": "OK",
1670+
"schema": {
1671+
"$ref": "#/definitions/v4.ExportResponse"
1672+
}
1673+
},
1674+
"500": {
1675+
"description": "Internal Server Error",
1676+
"schema": {
1677+
"$ref": "#/definitions/v4.ExportResponse"
1678+
}
1679+
}
1680+
}
1681+
},
1682+
"options": {
1683+
"description": "Returns an empty response with the HTTP Header \"allow\" set to the allowed HTTP verbs",
1684+
"tags": [
1685+
"Export"
1686+
],
1687+
"summary": "Allowed HTTP verbs",
1688+
"responses": {
1689+
"204": {
1690+
"description": "No Content"
1691+
}
1692+
}
1693+
}
1694+
},
16571695
"/v4/goals": {
16581696
"get": {
16591697
"description": "Returns a list of goals",
@@ -3449,7 +3487,7 @@
34493487
}
34503488
},
34513489
"error": {
3452-
"description": "FIMXE: make this *error for ALL responses",
3490+
"description": "The error, if any occurred",
34533491
"type": "string",
34543492
"example": "the specified resource ID is not a valid UUID"
34553493
}
@@ -4010,6 +4048,33 @@
40104048
}
40114049
}
40124050
},
4051+
"v4.ExportResponse": {
4052+
"type": "object",
4053+
"properties": {
4054+
"clacks": {
4055+
"description": "This will always have the value \"GNU Terry Pratchett\"",
4056+
"type": "string"
4057+
},
4058+
"creationTime": {
4059+
"description": "Time the export was created",
4060+
"type": "string"
4061+
},
4062+
"data": {
4063+
"description": "The exported data",
4064+
"type": "object",
4065+
"additionalProperties": {
4066+
"type": "array",
4067+
"items": {
4068+
"type": "integer"
4069+
}
4070+
}
4071+
},
4072+
"version": {
4073+
"description": "The version of the backend the export was made with",
4074+
"type": "string"
4075+
}
4076+
}
4077+
},
40134078
"v4.Goal": {
40144079
"type": "object",
40154080
"properties": {

api/swagger.yaml

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ definitions:
293293
$ref: '#/definitions/v4.BudgetResponse'
294294
type: array
295295
error:
296-
description: 'FIMXE: make this *error for ALL responses'
296+
description: The error, if any occurred
297297
example: the specified resource ID is not a valid UUID
298298
type: string
299299
type: object
@@ -701,6 +701,25 @@ definitions:
701701
example: the specified resource ID is not a valid UUID
702702
type: string
703703
type: object
704+
v4.ExportResponse:
705+
properties:
706+
clacks:
707+
description: This will always have the value "GNU Terry Pratchett"
708+
type: string
709+
creationTime:
710+
description: Time the export was created
711+
type: string
712+
data:
713+
additionalProperties:
714+
items:
715+
type: integer
716+
type: array
717+
description: The exported data
718+
type: object
719+
version:
720+
description: The version of the backend the export was made with
721+
type: string
722+
type: object
704723
v4.Goal:
705724
properties:
706725
amount:
@@ -2493,6 +2512,32 @@ paths:
24932512
summary: Update MonthConfig
24942513
tags:
24952514
- Envelopes
2515+
/v4/export:
2516+
get:
2517+
description: Exports all resources for the instance
2518+
produces:
2519+
- application/json
2520+
responses:
2521+
"200":
2522+
description: OK
2523+
schema:
2524+
$ref: '#/definitions/v4.ExportResponse'
2525+
"500":
2526+
description: Internal Server Error
2527+
schema:
2528+
$ref: '#/definitions/v4.ExportResponse'
2529+
summary: Export
2530+
tags:
2531+
- Export
2532+
options:
2533+
description: Returns an empty response with the HTTP Header "allow" set to the
2534+
allowed HTTP verbs
2535+
responses:
2536+
"204":
2537+
description: No Content
2538+
summary: Allowed HTTP verbs
2539+
tags:
2540+
- Export
24962541
/v4/goals:
24972542
get:
24982543
description: Returns a list of goals

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/envelope-zero/backend/v5
22

3-
go 1.20
3+
go 1.22
44

55
require (
66
github.com/gin-contrib/cors v1.7.1

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcP
2727
github.com/gin-contrib/cors v1.7.1 h1:s9SIppU/rk8enVvkzwiC2VK3UZ/0NNGsWfUKvV55rqs=
2828
github.com/gin-contrib/cors v1.7.1/go.mod h1:n/Zj7B4xyrgk/cX1WCX2dkzFfaNm/xJb6oIUk7WTtps=
2929
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
30+
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
3031
github.com/gin-contrib/logger v1.1.1 h1:78Qzfpx3JvpnNX/6bErifIcnFqwbVKl2gS5WGExFPOs=
3132
github.com/gin-contrib/logger v1.1.1/go.mod h1:Cmqqcc6Yce4+r776VET1AcU4ydAR6sMKuDtBKLje20Y=
3233
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
@@ -59,6 +60,7 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB
5960
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
6061
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
6162
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
63+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
6264
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
6365
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
6466
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@@ -75,8 +77,10 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
7577
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
7678
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
7779
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
80+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
7881
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
7982
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
83+
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
8084
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8185
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
8286
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -95,6 +99,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
9599
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
96100
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
97101
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
102+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
98103
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
99104
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
100105
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -140,6 +145,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
140145
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
141146
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
142147
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
148+
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
143149
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
144150
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
145151
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -185,6 +191,7 @@ golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5C
185191
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
186192
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
187193
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
194+
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
188195
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
189196
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
190197
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=

pkg/controllers/v4/budget_types.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ type BudgetListResponse struct {
6565
}
6666

6767
type BudgetCreateResponse struct {
68-
// FIMXE: make this *error for ALL responses
6968
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred
7069
Data []BudgetResponse `json:"data"` // List of created Budgets
7170
}

pkg/controllers/v4/export.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package v4
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"reflect"
7+
"time"
8+
9+
"github.com/envelope-zero/backend/v5/pkg/httputil"
10+
"github.com/envelope-zero/backend/v5/pkg/models"
11+
"github.com/gin-gonic/gin"
12+
)
13+
14+
var backendVersion string
15+
16+
func RegisterExportRoutes(r *gin.RouterGroup, version string) {
17+
backendVersion = version
18+
19+
{
20+
r.OPTIONS("", OptionsExport)
21+
r.GET("", GetExport)
22+
}
23+
}
24+
25+
// @Summary Allowed HTTP verbs
26+
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
27+
// @Tags Export
28+
// @Success 204
29+
// @Router /v4/export [options]
30+
func OptionsExport(c *gin.Context) {
31+
httputil.OptionsGet(c)
32+
}
33+
34+
// @Summary Export
35+
// @Description Exports all resources for the instance
36+
// @Tags Export
37+
// @Produce json
38+
// @Success 200 {object} ExportResponse
39+
// @Failure 500 {object} ExportResponse
40+
// @Router /v4/export [get]
41+
func GetExport(c *gin.Context) {
42+
resources := make(map[string]json.RawMessage)
43+
44+
for _, model := range models.Registry {
45+
b, err := model.Export()
46+
if err != nil {
47+
c.JSON(status(err), httpError{
48+
Error: err.Error(),
49+
})
50+
return
51+
}
52+
53+
resources[reflect.TypeOf(model).Name()] = b
54+
}
55+
56+
c.JSON(http.StatusOK, ExportResponse{
57+
Version: backendVersion,
58+
Data: resources,
59+
CreationTime: time.Now(),
60+
Clacks: "GNU Terry Pratchett",
61+
})
62+
}

0 commit comments

Comments
 (0)