| package entitydiff |
|
|
| import ( |
| "errors" |
|
|
| "github.com/samber/lo" |
|
|
| "github.com/openmeterio/openmeter/pkg/equal" |
| ) |
|
|
| type Entity interface { |
| GetID() string |
| IsDeleted() bool |
| } |
|
|
| type DiffUpdate[T Entity] struct { |
| |
| PersistedState T |
| |
| ExpectedState T |
| } |
|
|
| type speculativeDiff[T Entity] struct { |
| UpdateCandidates []DiffUpdate[T] |
| Create []T |
| Delete []T |
| } |
|
|
| type Diff[T Entity] struct { |
| Update []DiffUpdate[T] |
| Create []T |
| Delete []T |
| } |
|
|
| func (d *Diff[T]) NeedsUpdate(item ...DiffUpdate[T]) { |
| d.Update = append(d.Update, item...) |
| } |
|
|
| func (d *Diff[T]) NeedsCreate(item ...T) { |
| d.Create = append(d.Create, item...) |
| } |
|
|
| func (d *Diff[T]) NeedsDelete(item ...T) { |
| d.Delete = append(d.Delete, item...) |
| } |
|
|
| func (d *Diff[T]) Append(a Diff[T]) Diff[T] { |
| out := Diff[T]{ |
| Delete: make([]T, 0, len(a.Delete)+len(d.Delete)), |
| Update: make([]DiffUpdate[T], 0, len(a.Update)+len(d.Update)), |
| Create: make([]T, 0, len(a.Create)+len(d.Create)), |
| } |
|
|
| out.Delete = append(out.Delete, a.Delete...) |
| out.Delete = append(out.Delete, d.Delete...) |
|
|
| out.Update = append(out.Update, a.Update...) |
| out.Update = append(out.Update, d.Update...) |
|
|
| out.Create = append(out.Create, a.Create...) |
| out.Create = append(out.Create, d.Create...) |
|
|
| return out |
| } |
|
|
| func (d *Diff[T]) IsEmpty() bool { |
| return len(d.Delete) == 0 && len(d.Update) == 0 && len(d.Create) == 0 |
| } |
|
|
| func Union[T Entity](diffs ...Diff[T]) Diff[T] { |
| out := Diff[T]{ |
| Create: []T{}, |
| Delete: []T{}, |
| Update: []DiffUpdate[T]{}, |
| } |
|
|
| for _, diff := range diffs { |
| out.Create = append(out.Create, diff.Create...) |
| out.Delete = append(out.Delete, diff.Delete...) |
| out.Update = append(out.Update, diff.Update...) |
| } |
|
|
| return out |
| } |
|
|
| |
| |
| |
| |
| func diffByID[T Entity](expectedState, dbState []T) speculativeDiff[T] { |
| diff := speculativeDiff[T]{} |
|
|
| itemsWithID := lo.Filter(dbState, func(item T, _ int) bool { |
| return item.GetID() != "" |
| }) |
|
|
| dbStateByID := lo.SliceToMap(itemsWithID, func(item T) (string, T) { |
| return item.GetID(), item |
| }) |
|
|
| for _, expected := range expectedState { |
| if expected.GetID() == "" { |
| |
| if expected.IsDeleted() { |
| |
| continue |
| } |
|
|
| |
| diff.Create = append(diff.Create, expected) |
| continue |
| } |
|
|
| dbState, ok := dbStateByID[expected.GetID()] |
| if !ok { |
| if expected.IsDeleted() { |
| |
| continue |
| } |
|
|
| |
| diff.Create = append(diff.Create, expected) |
| continue |
| } |
|
|
| if expected.IsDeleted() { |
| if !dbState.IsDeleted() { |
| |
| |
| |
| |
| |
| |
|
|
| diff.Delete = append(diff.Delete, expected) |
| continue |
| } |
|
|
| |
| continue |
| } |
|
|
| diff.UpdateCandidates = append(diff.UpdateCandidates, DiffUpdate[T]{ |
| PersistedState: dbState, |
| ExpectedState: expected, |
| }) |
| } |
|
|
| |
| expectedItemsByID := lo.SliceToMap( |
| lo.Filter(expectedState, func(item T, _ int) bool { |
| return item.GetID() != "" |
| }), |
| func(item T) (string, T) { |
| return item.GetID(), item |
| }, |
| ) |
|
|
| for dbID, dbItemState := range dbStateByID { |
| |
| if _, ok := expectedItemsByID[dbID]; !ok { |
| diff.Delete = append(diff.Delete, dbItemState) |
| } |
| } |
|
|
| return diff |
| } |
|
|
| type DiffByIDInput[T Entity] struct { |
| DBState []T |
| ExpectedState []T |
|
|
| HandleDelete func(item T) error |
| HandleCreate func(item T) error |
| HandleUpdate func(item DiffUpdate[T]) error |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func DiffByID[T Entity](input DiffByIDInput[T]) error { |
| diff := diffByID(input.ExpectedState, input.DBState) |
|
|
| var errs []error |
| for _, delete := range diff.Delete { |
| if input.HandleDelete != nil { |
| if err := input.HandleDelete(delete); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| } |
|
|
| for _, expected := range diff.Create { |
| if input.HandleCreate != nil { |
| if err := input.HandleCreate(expected); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| } |
|
|
| for _, update := range diff.UpdateCandidates { |
| if input.HandleUpdate != nil { |
| if err := input.HandleUpdate(update); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| } |
|
|
| return errors.Join(errs...) |
| } |
|
|
| type EqualerEntity[T any] interface { |
| Entity |
| equal.Equaler[T] |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func DiffByIDEqualer[T EqualerEntity[T]](expectedState, dbState []T) Diff[T] { |
| diff := diffByID(expectedState, dbState) |
|
|
| out := Diff[T]{ |
| Create: diff.Create, |
| Delete: diff.Delete, |
| Update: make([]DiffUpdate[T], 0, len(diff.UpdateCandidates)), |
| } |
|
|
| for _, update := range diff.UpdateCandidates { |
| if !update.PersistedState.Equal(update.ExpectedState) { |
| out.Update = append(out.Update, update) |
| } |
| } |
|
|
| return out |
| } |
|
|