-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #178 from calvinmclean/feature/migrate
Add migrate feature for changes to resources
- Loading branch information
Showing
15 changed files
with
689 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/calvinmclean/automated-garden/garden-app/pkg/storage" | ||
"github.com/calvinmclean/automated-garden/garden-app/server" | ||
"github.com/spf13/cobra" | ||
"github.com/spf13/viper" | ||
) | ||
|
||
var migrateCommand = &cobra.Command{ | ||
Use: "migrate", | ||
Short: "Run storage migrations to update all resources", | ||
RunE: func(cmd *cobra.Command, _ []string) error { | ||
var config server.Config | ||
err := viper.Unmarshal(&config) | ||
if err != nil { | ||
return fmt.Errorf("unable to read config from file: %w", err) | ||
} | ||
|
||
storageClient, err := storage.NewClient(config.StorageConfig) | ||
if err != nil { | ||
return fmt.Errorf("unable to initialize storage client: %v", err) | ||
} | ||
|
||
return storageClient.RunMigrations(cmd.Context()) | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,12 @@ | ||
Garden_chokmn1nhf81274ru2mg: '{"name":"Indoor Seed Starting","topic_prefix":"seed-garden","id":"chokmn1nhf81274ru2mg","max_zones":3,"created_at":"2023-05-27T00:14:20.324Z","light_schedule":{"duration":"16h0m0s","start_time":"06:00:00-07:00"}}' | ||
Garden_cos1pt0n1e43o39cs40g: '{"name":"Front Yard","topic_prefix":"front-yard","id":"cos1pt0n1e43o39cs40g","max_zones":4,"created_at":"2024-05-05T16:58:10.206246-07:00"}' | ||
WaterSchedule_chokmq1nhf81274ru2n0: '{"id":"chokmq1nhf81274ru2n0","duration":"1s","interval":"30s","start_time":"15:00:00Z","end_date":"2024-05-05T17:01:10.335976-07:00","name":"WaterEvery30s"}' | ||
WaterSchedule_cii72s9nhf8f7gdpckug: '{"id":"cii72s9nhf8f7gdpckug","duration":"1h0m0s","interval":"480h0m0s","start_time":"12:00:00Z","name":"Winter Trees","description":"Water deeply and infrequently in the winter","active_period":{"start_month":"October","end_month":"March"}}' | ||
WaterSchedule_cjbg22a8tio6of9s8o0g: '{"id":"cjbg22a8tio6of9s8o0g","duration":"30s","interval":"24h0m0s","start_time":"15:00:00Z","name":"Seedlings","description":"Water seedlings a bit every day"}' | ||
WaterSchedule_cos1s28n1e43sc2vb4k0: '{"id":"cos1s28n1e43sc2vb4k0","duration":"1h30m0s","interval":"240h0m0s","start_time":"12:00:00Z","name":"Summer Trees","description":"Water deeply every 10 days","active_period":{"start_month":"April","end_month":"September"}}' | ||
WaterSchedule_cos1suon1e43sc2vb4kg: '{"id":"cos1suon1e43sc2vb4kg","duration":"45m0s","interval":"120h0m0s","start_time":"15:04:05Z","name":"Shrubs","description":"Water shrubs every 5 days"}' | ||
Zone_chokn19nhf81274ru2o0: | | ||
{ | ||
"name": "Zone 1", | ||
"id": "chokn19nhf81274ru2o0", | ||
"garden_id": "chokmn1nhf81274ru2mg", | ||
"position": 0, | ||
"created_at": "2023-05-27T00:15:01.683Z", | ||
"water_schedule_ids": [ | ||
"cjbg22a8tio6of9s8o0g" | ||
] | ||
} | ||
Zone_cij42vpnhf85d3acsgu0: '{"name":"Zone 2","id":"cij42vpnhf85d3acsgu0","garden_id":"chokmn1nhf81274ru2mg","position":0,"created_at":"2023-07-06T04:22:23.331Z","end_date":"2024-05-05T17:00:29.204187-07:00","water_schedule_ids":["chokmq1nhf81274ru2n0"],"skip_count":null}' | ||
Zone_cij436pnhf85d3acsgug: | | ||
{ | ||
"name": "Zone 1", | ||
"id": "cij436pnhf85d3acsgug", | ||
"garden_id": "cihpp51nhf84tr94jtfg", | ||
"position": 0, | ||
"created_at": "2023-07-06T04:22:51.608Z", | ||
"water_schedule_ids": [ | ||
"cii72s9nhf8f7gdpckug" | ||
], | ||
"skip_count": null | ||
} | ||
Zone_cos1q8gn1e43o39cs410: '{"name":"Trees","details":{"description":"This zone controls watering to two trees that are watered deeply"},"id":"cos1q8gn1e43o39cs410","garden_id":"cos1pt0n1e43o39cs40g","position":0,"created_at":"2024-05-05T16:58:49.357958-07:00","water_schedule_ids":["cos1s28n1e43sc2vb4k0","cii72s9nhf8f7gdpckug"],"skip_count":null}' | ||
Zone_cos1qf0n1e43o39cs41g: '{"name":"Shrubs","details":{"description":"This zone has a few shrubs that need water more frequently"},"id":"cos1qf0n1e43o39cs41g","garden_id":"cos1pt0n1e43o39cs40g","position":2,"created_at":"2024-05-05T16:59:08.973836-07:00","water_schedule_ids":["cos1suon1e43sc2vb4kg"],"skip_count":null}' | ||
Garden_chokmn1nhf81274ru2mg: '{"name":"Indoor Seed Starting","topic_prefix":"seed-garden","id":"chokmn1nhf81274ru2mg","max_zones":3,"created_at":"2023-05-27T00:14:20.324Z","light_schedule":{"duration":"16h0m0s","start_time":"06:00:00-07:00"},"version":1}' | ||
Garden_cos1pt0n1e43o39cs40g: '{"name":"Front Yard","topic_prefix":"front-yard","id":"cos1pt0n1e43o39cs40g","max_zones":4,"created_at":"2024-05-05T16:58:10.206246-07:00","version":1}' | ||
WaterSchedule_chokmq1nhf81274ru2n0: '{"id":"chokmq1nhf81274ru2n0","duration":"1s","interval":"30s","start_date":null,"start_time":"15:00:00Z","end_date":"2024-05-05T17:01:10.335976-07:00","name":"WaterEvery30s","version":1}' | ||
WaterSchedule_cii72s9nhf8f7gdpckug: '{"id":"cii72s9nhf8f7gdpckug","duration":"1h0m0s","interval":"480h0m0s","start_date":null,"start_time":"12:00:00Z","name":"Winter Trees","description":"Water deeply and infrequently in the winter","active_period":{"start_month":"October","end_month":"March"},"version":1}' | ||
WaterSchedule_cjbg22a8tio6of9s8o0g: '{"id":"cjbg22a8tio6of9s8o0g","duration":"30s","interval":"24h0m0s","start_date":null,"start_time":"15:00:00Z","name":"Seedlings","description":"Water seedlings a bit every day","version":1}' | ||
WaterSchedule_cos1s28n1e43sc2vb4k0: '{"id":"cos1s28n1e43sc2vb4k0","duration":"1h30m0s","interval":"240h0m0s","start_date":null,"start_time":"12:00:00Z","name":"Summer Trees","description":"Water deeply every 10 days","active_period":{"start_month":"April","end_month":"September"},"version":1}' | ||
WaterSchedule_cos1suon1e43sc2vb4kg: '{"id":"cos1suon1e43sc2vb4kg","duration":"45m0s","interval":"120h0m0s","start_date":null,"start_time":"15:04:05Z","name":"Shrubs","description":"Water shrubs every 5 days","version":1}' | ||
Zone_chokn19nhf81274ru2o0: '{"name":"Zone 1","id":"chokn19nhf81274ru2o0","garden_id":"chokmn1nhf81274ru2mg","position":0,"created_at":"2023-05-27T00:15:01.683Z","water_schedule_ids":["cjbg22a8tio6of9s8o0g"],"skip_count":null,"version":1}' | ||
Zone_cij42vpnhf85d3acsgu0: '{"name":"Zone 2","id":"cij42vpnhf85d3acsgu0","garden_id":"chokmn1nhf81274ru2mg","position":0,"created_at":"2023-07-06T04:22:23.331Z","end_date":"2024-05-05T17:00:29.204187-07:00","water_schedule_ids":["chokmq1nhf81274ru2n0"],"skip_count":null,"version":1}' | ||
Zone_cij436pnhf85d3acsgug: '{"name":"Zone 1","id":"cij436pnhf85d3acsgug","garden_id":"cihpp51nhf84tr94jtfg","position":0,"created_at":"2023-07-06T04:22:51.608Z","water_schedule_ids":["cii72s9nhf8f7gdpckug"],"skip_count":null,"version":1}' | ||
Zone_cos1q8gn1e43o39cs410: '{"name":"Trees","details":{"description":"This zone controls watering to two trees that are watered deeply"},"id":"cos1q8gn1e43o39cs410","garden_id":"cos1pt0n1e43o39cs40g","position":0,"created_at":"2024-05-05T16:58:49.357958-07:00","water_schedule_ids":["cos1s28n1e43sc2vb4k0","cii72s9nhf8f7gdpckug"],"skip_count":null,"version":1}' | ||
Zone_cos1qf0n1e43o39cs41g: '{"name":"Shrubs","details":{"description":"This zone has a few shrubs that need water more frequently"},"id":"cos1qf0n1e43o39cs41g","garden_id":"cos1pt0n1e43o39cs40g","position":2,"created_at":"2024-05-05T16:59:08.973836-07:00","water_schedule_ids":["cos1suon1e43sc2vb4kg"],"skip_count":null,"version":1}' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
module github.com/calvinmclean/automated-garden/garden-app | ||
|
||
go 1.22 | ||
go 1.23 | ||
|
||
toolchain go1.23.0 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package migrate | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
) | ||
|
||
var ( | ||
ErrNotFound = errors.New("migration not found") | ||
ErrInvalidToType = errors.New("unexpected To type") | ||
ErrInvalidFromType = errors.New("unexpected From type") | ||
) | ||
|
||
type Error struct { | ||
Err error | ||
Name string | ||
Version uint | ||
} | ||
|
||
func (e Error) Unwrap() error { | ||
return e.Err | ||
} | ||
|
||
func (e Error) Error() string { | ||
return fmt.Sprintf("error running migration %q/%d: %s", e.Name, e.Version, e.Err.Error()) | ||
} | ||
|
||
func errNotFound(version uint) Error { | ||
return Error{ErrNotFound, "Unknown", version} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package migrate | ||
|
||
import ( | ||
"iter" | ||
) | ||
|
||
type Versioned interface { | ||
GetVersion() uint | ||
} | ||
|
||
type IncrementVersion interface { | ||
SetVersion(uint) | ||
} | ||
|
||
type Migration interface { | ||
Migrate(Versioned) (any, error) | ||
Name() string | ||
} | ||
|
||
type Func[From, To Versioned] func(From) (To, error) | ||
|
||
type migration[From, To Versioned] struct { | ||
name string | ||
migrate Func[From, To] | ||
} | ||
|
||
func (m *migration[From, To]) Name() string { | ||
return m.name | ||
} | ||
|
||
func (m *migration[From, To]) Migrate(from Versioned) (any, error) { | ||
f, ok := from.(From) | ||
if !ok { | ||
return nil, ErrInvalidFromType | ||
} | ||
|
||
return m.migrate(f) | ||
} | ||
|
||
func NewMigration[From, To Versioned](name string, migrate Func[From, To]) Migration { | ||
return &migration[From, To]{ | ||
name: name, | ||
migrate: migrate, | ||
} | ||
} | ||
|
||
func All[From, To Versioned](migrations []Migration, from []From) ([]To, error) { | ||
result := []To{} | ||
|
||
for _, f := range from { | ||
to, err := migrateToFinalVersion[From, To](migrations, f) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
result = append(result, to) | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
func Each[From, To Versioned](migrations []Migration, from []From) iter.Seq2[To, error] { | ||
return func(yield func(To, error) bool) { | ||
for _, f := range from { | ||
to, err := migrateToFinalVersion[From, To](migrations, f) | ||
shouldContinue := yield(to, err) | ||
if !shouldContinue { | ||
return | ||
} | ||
} | ||
} | ||
} | ||
|
||
func runMigration[From, To Versioned](migrations []Migration, from From) (To, error) { | ||
v := from.GetVersion() | ||
|
||
if v >= uint(len(migrations)) { | ||
return *new(To), errNotFound(v) | ||
} | ||
m := migrations[v] | ||
|
||
to, err := m.Migrate(from) | ||
if err != nil { | ||
return *new(To), Error{err, m.Name(), v} | ||
} | ||
|
||
if versionSetter, ok := to.(IncrementVersion); ok { | ||
versionSetter.SetVersion(v + 1) | ||
} | ||
|
||
out, ok := to.(To) | ||
if !ok { | ||
return *new(To), Error{ErrInvalidToType, m.Name(), from.GetVersion()} | ||
} | ||
|
||
return out, err | ||
} | ||
|
||
func migrateToFinalVersion[From, To Versioned](migrations []Migration, from From) (To, error) { | ||
var next Versioned | ||
var err error | ||
|
||
next = from | ||
for { | ||
next, err = runMigration[Versioned, Versioned](migrations, next) | ||
if err != nil { | ||
return *new(To), err | ||
} | ||
|
||
result, ok := next.(To) | ||
if ok { | ||
return result, nil | ||
} | ||
} | ||
} |
Oops, something went wrong.