openmeter / pkg /entitydiff /diff.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 9)
fea99b3 verified
Raw
History Blame Contribute Delete
6.47 kB
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 is the persisted state of the entity (e.g. in database or any storage), if empty there is no persisted state
PersistedState T
// ExpectedState is the expected state of the entity (e.g. after the changes done by the backend)
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
}
// diffByID compares the expected state with the db state by ID.
// It returns the UpdateCandidates as it does not compare entities. This call should be used for entities with child entities.
//
// If the entity does not have a child, please use DiffByIDEqualer instead.
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 the expected state has no id, we cannot correlate it with the db state
if expected.IsDeleted() {
// If the expected state is deleted, we can skip it
continue
}
// If the expected state is not deleted, we need to create it
diff.Create = append(diff.Create, expected)
continue
}
dbState, ok := dbStateByID[expected.GetID()]
if !ok {
if expected.IsDeleted() {
// If the expected state is deleted, but we don't have it in the db, we can skip it
continue
}
// If the expected state is not deleted, but we don't have it in the db, we need to create it
diff.Create = append(diff.Create, expected)
continue
}
if expected.IsDeleted() {
if !dbState.IsDeleted() {
// If the expected state is deleted, but we have it in the db, we need to delete it, based on the target state
// so that if somebody not just set the deleted_at field, but also some other fields that should be persisted
// to the database.
//
// For example if you delete a line that is subscription synced, the edit will cause managedBy to become manual
// in the same change as the deleted_at change.
diff.Delete = append(diff.Delete, expected)
continue
}
// If the expected state is deleted, and we have it in the db, but it's deleted, we can skip it
continue
}
diff.UpdateCandidates = append(diff.UpdateCandidates, DiffUpdate[T]{
PersistedState: dbState,
ExpectedState: expected,
})
}
// Let's collect expected items with an ID so that we can check if they would require deletion
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 the expected state does not contain the item we need to delete it
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
}
// DiffByID compares the expected state with the db state by ID.
//
// The callback functions are used to allow for custom handling of the diff entries.
//
// The call does not compare the entities themselves, it only compares the IDs. Can be used for
// entities that have child entities.
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]
}
// DiffByIDEqualer compares the expected state with the db state by ID.
//
// This call uses the Equal method of the entity to compare the expected state with the db state in
// case of updates.
//
// Should be used for entities that do not have any child entities.
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
}