Added a ClassificationsUpdated event
SaveOpportunities now reports the classification codes a save brings into
use (AddedClassifications, in full) and takes out of use
(RemovedClassificationIDs), by comparing the codes in use before and after
the save inside its transaction. The fetcher publishes them as a
ClassificationsUpdated event, {"added":[codes],"removed":[ids]}, never null,
before the save's OpportunitiesUpdated, so clients can keep their list of
classifications without asking the API again.
This commit is contained in:
1 parent
aa06b94870
commit
51d6470649
6 files changed
+287
-3
No files matched your search
+69
-1
@@ -61,6 +61,14 @@ type SaveMultipleResult struct {
|
||||
UpdatedOpportunities int
|
||||
// OpportunityResults is a slice of the results of each opportunity saved.
|
||||
OpportunityResults []SaveResult
|
||||
// AddedClassifications are the classification codes that no stored
|
||||
// opportunity had before the save and at least one has after it, ordered
|
||||
// by code; nil if there are none.
|
||||
AddedClassifications []types.ClassificationCode
|
||||
// RemovedClassificationIDs are the ids of the classification codes that
|
||||
// a stored opportunity had before the save and none has after it, in
|
||||
// code order; nil if there are none.
|
||||
RemovedClassificationIDs []int64
|
||||
}
|
||||
|
||||
// SaveResult reports what SaveOpportunity changed.
|
||||
@@ -297,7 +305,9 @@ func (s *Store) SaveOpportunity(
|
||||
// SetMarks to change them).
|
||||
//
|
||||
// Each opportunity is normalised in place (see prepareOpportunity), and when
|
||||
// it is written its classifications' IDs are filled in.
|
||||
// it is written its classifications' IDs are filled in. The result reports
|
||||
// the classification codes the save brought into or out of use (see
|
||||
// ListClassifications).
|
||||
func (s *Store) SaveOpportunities(
|
||||
ctx context.Context, opportunities []*types.Opportunity,
|
||||
) (SaveMultipleResult, error) {
|
||||
@@ -313,6 +323,11 @@ func (s *Store) SaveOpportunities(
|
||||
}
|
||||
defer tx.Rollback() //nolint:errcheck // no-op after Commit
|
||||
|
||||
before, err := classificationsInUse(ctx, tx)
|
||||
if err != nil {
|
||||
return SaveMultipleResult{}, err
|
||||
}
|
||||
|
||||
total := SaveMultipleResult{
|
||||
OpportunityResults: make([]SaveResult, len(opportunities)),
|
||||
}
|
||||
@@ -331,6 +346,14 @@ func (s *Store) SaveOpportunities(
|
||||
}
|
||||
}
|
||||
|
||||
after, err := classificationsInUse(ctx, tx)
|
||||
if err != nil {
|
||||
return SaveMultipleResult{}, err
|
||||
}
|
||||
added, removed := classificationChanges(before, after)
|
||||
total.AddedClassifications = added
|
||||
total.RemovedClassificationIDs = removed
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return SaveMultipleResult{}, err
|
||||
}
|
||||
@@ -338,6 +361,51 @@ func (s *Store) SaveOpportunities(
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// classificationsInUse returns the classification codes at least one
|
||||
// opportunity has, ordered by code, as tx sees them.
|
||||
func classificationsInUse(
|
||||
ctx context.Context, tx *sqlx.Tx,
|
||||
) ([]types.ClassificationCode, error) {
|
||||
var classifications []types.ClassificationCode
|
||||
err := tx.SelectContext(ctx, &classifications, selectClassificationsInUse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing classifications: %w", err)
|
||||
}
|
||||
|
||||
return classifications, nil
|
||||
}
|
||||
|
||||
// classificationChanges compares the classification codes in use before and
|
||||
// after a save: it returns the codes in after but not before, and the ids of
|
||||
// those in before but not after, each in the order they're given.
|
||||
func classificationChanges(
|
||||
before, after []types.ClassificationCode,
|
||||
) ([]types.ClassificationCode, []int64) {
|
||||
wasInUse := make(map[int64]bool, len(before))
|
||||
for _, classification := range before {
|
||||
wasInUse[classification.ID] = true
|
||||
}
|
||||
isInUse := make(map[int64]bool, len(after))
|
||||
for _, classification := range after {
|
||||
isInUse[classification.ID] = true
|
||||
}
|
||||
|
||||
var added []types.ClassificationCode
|
||||
for _, classification := range after {
|
||||
if !wasInUse[classification.ID] {
|
||||
added = append(added, classification)
|
||||
}
|
||||
}
|
||||
var removed []int64
|
||||
for _, classification := range before {
|
||||
if !isInUse[classification.ID] {
|
||||
removed = append(removed, classification.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return added, removed
|
||||
}
|
||||
|
||||
// saveOpportunity does the work of SaveOpportunity and SaveOpportunities for
|
||||
// one prepared opportunity inside tx. isNew reports that the opportunity was
|
||||
// not already stored.
|
||||
|
||||
@@ -944,3 +944,69 @@ func TestListClassifications(t *testing.T) {
|
||||
t.Errorf("after the change: codes = %s; want 03000000", got)
|
||||
}
|
||||
}
|
||||
|
||||
// SaveOpportunities reports the classification codes a save brings into use
|
||||
// and those it takes out of use, and none when the set doesn't change.
|
||||
func TestSaveOpportunitiesClassificationChanges(t *testing.T) {
|
||||
s := openStore(t)
|
||||
ctx := context.Background()
|
||||
save := func(opportunity *types.Opportunity) store.SaveMultipleResult {
|
||||
t.Helper()
|
||||
total, err := s.SaveOpportunities(ctx, []*types.Opportunity{opportunity})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return total
|
||||
}
|
||||
codesOf := func(classifications []types.ClassificationCode) string {
|
||||
var codes []string
|
||||
for _, classification := range classifications {
|
||||
codes = append(codes, classification.Code)
|
||||
}
|
||||
return strings.Join(codes, ",")
|
||||
}
|
||||
|
||||
total := save(opportunity("ocds-1", "First", 1, "r1"))
|
||||
if codesOf(total.AddedClassifications) != "03000000,72000000" ||
|
||||
total.RemovedClassificationIDs != nil {
|
||||
t.Fatalf(
|
||||
"first save: added %v, removed %v; want both codes added",
|
||||
total.AddedClassifications, total.RemovedClassificationIDs,
|
||||
)
|
||||
}
|
||||
removedID := total.AddedClassifications[1].ID // 72000000
|
||||
|
||||
total = save(opportunity("ocds-2", "Second", 1, "r1"))
|
||||
if total.AddedClassifications != nil ||
|
||||
total.RemovedClassificationIDs != nil {
|
||||
t.Errorf(
|
||||
"codes already in use: added %v, removed %v; want none",
|
||||
total.AddedClassifications, total.RemovedClassificationIDs,
|
||||
)
|
||||
}
|
||||
|
||||
// ocds-1 drops 72000000, which ocds-2 still has: still in use.
|
||||
newer := opportunity("ocds-1", "Newer", 2, "r2")
|
||||
newer.Classifications = newer.Classifications[1:]
|
||||
total = save(newer)
|
||||
if total.AddedClassifications != nil ||
|
||||
total.RemovedClassificationIDs != nil {
|
||||
t.Errorf(
|
||||
"code still in use: added %v, removed %v; want none",
|
||||
total.AddedClassifications, total.RemovedClassificationIDs,
|
||||
)
|
||||
}
|
||||
|
||||
// ocds-2 drops it too: it goes out of use.
|
||||
newer = opportunity("ocds-2", "Newer", 2, "r2")
|
||||
newer.Classifications = newer.Classifications[1:]
|
||||
total = save(newer)
|
||||
if total.AddedClassifications != nil ||
|
||||
len(total.RemovedClassificationIDs) != 1 ||
|
||||
total.RemovedClassificationIDs[0] != removedID {
|
||||
t.Errorf(
|
||||
"code out of use: added %v, removed %v; want removed [%d]",
|
||||
total.AddedClassifications, total.RemovedClassificationIDs, removedID,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user