184 lines
4.8 KiB
Go
184 lines
4.8 KiB
Go
package mongo
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
|
|
|
"github.com/andydixon/relata/internal/domain"
|
|
)
|
|
|
|
// ClaimRepo stores extracted claims.
|
|
type ClaimRepo struct{ base }
|
|
|
|
var _ domain.ClaimRepository = (*ClaimRepo)(nil)
|
|
|
|
// ReplaceForItem swaps an item's claims for a new set, making claim extraction
|
|
// idempotent across reprocessing runs.
|
|
func (r *ClaimRepo) ReplaceForItem(ctx context.Context, workspaceID, itemID string, claims []*domain.Claim) error {
|
|
ctx, cancel := r.ctx(ctx)
|
|
defer cancel()
|
|
started := time.Now()
|
|
|
|
f, err := wsFilter(workspaceID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f = append(f, bson.E{Key: "item_id", Value: itemID})
|
|
|
|
if _, err := r.coll.DeleteMany(ctx, f); err != nil {
|
|
r.observe("replace_for_item", started, err)
|
|
return translate(err)
|
|
}
|
|
if len(claims) == 0 {
|
|
r.observe("replace_for_item", started, nil)
|
|
return nil
|
|
}
|
|
|
|
// Deduplicate within the batch: two chunks can yield the same claim, and
|
|
// the unique dedupe_key index would otherwise reject the whole insert.
|
|
seen := map[string]bool{}
|
|
docs := make([]any, 0, len(claims))
|
|
for _, c := range claims {
|
|
if c.WorkspaceID != workspaceID || c.ItemID != itemID {
|
|
return domain.ErrWorkspaceMiss
|
|
}
|
|
if seen[c.DedupeKey] {
|
|
continue
|
|
}
|
|
seen[c.DedupeKey] = true
|
|
docs = append(docs, c)
|
|
}
|
|
|
|
_, err = r.coll.InsertMany(ctx, docs, options.InsertMany().SetOrdered(false))
|
|
if isDup(err) {
|
|
// A concurrent run inserted an identical claim; that is the intended
|
|
// outcome, not a failure.
|
|
err = nil
|
|
}
|
|
r.observe("replace_for_item", started, err)
|
|
return translate(err)
|
|
}
|
|
|
|
// ListByItem returns an item's claims.
|
|
func (r *ClaimRepo) ListByItem(ctx context.Context, workspaceID, itemID string) ([]*domain.Claim, error) {
|
|
ctx, cancel := r.ctx(ctx)
|
|
defer cancel()
|
|
started := time.Now()
|
|
|
|
f, err := wsFilter(workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f = append(f, bson.E{Key: "item_id", Value: itemID})
|
|
|
|
cur, err := r.coll.Find(ctx, f, options.Find().SetSort(bson.D{{Key: "created_at", Value: 1}}))
|
|
r.observe("list_by_item", started, err)
|
|
if err != nil {
|
|
return nil, translate(err)
|
|
}
|
|
var out []*domain.Claim
|
|
if err := cur.All(ctx, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// FindBySubjects locates claims about the same coarse subject in other items.
|
|
//
|
|
// This is how support and contradiction candidates are found without comparing
|
|
// every claim to every other claim: the subject key is indexed, so the search
|
|
// is a bounded lookup rather than a scan.
|
|
func (r *ClaimRepo) FindBySubjects(ctx context.Context, workspaceID string, subjects []string, excludeItem string, limit int) ([]*domain.Claim, error) {
|
|
if len(subjects) == 0 {
|
|
return nil, nil
|
|
}
|
|
ctx, cancel := r.ctx(ctx)
|
|
defer cancel()
|
|
started := time.Now()
|
|
|
|
f, err := wsFilter(workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f = append(f, bson.E{Key: "subject", Value: bson.D{{Key: "$in", Value: stringsToAny(subjects)}}})
|
|
if excludeItem != "" {
|
|
f = append(f, bson.E{Key: "item_id", Value: bson.D{{Key: "$ne", Value: excludeItem}}})
|
|
}
|
|
|
|
if limit <= 0 || limit > 500 {
|
|
limit = 100
|
|
}
|
|
cur, err := r.coll.Find(ctx, f, options.Find().SetLimit(int64(limit)))
|
|
r.observe("find_by_subjects", started, err)
|
|
if err != nil {
|
|
return nil, translate(err)
|
|
}
|
|
var out []*domain.Claim
|
|
if err := cur.All(ctx, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// GetMany fetches claims by ID within a workspace.
|
|
func (r *ClaimRepo) GetMany(ctx context.Context, workspaceID string, claimIDs []string) ([]*domain.Claim, error) {
|
|
if len(claimIDs) == 0 {
|
|
return nil, nil
|
|
}
|
|
ctx, cancel := r.ctx(ctx)
|
|
defer cancel()
|
|
started := time.Now()
|
|
|
|
f, err := wsFilter(workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f = append(f, bson.E{Key: "_id", Value: bson.D{{Key: "$in", Value: stringsToAny(claimIDs)}}})
|
|
|
|
cur, err := r.coll.Find(ctx, f)
|
|
r.observe("get_many", started, err)
|
|
if err != nil {
|
|
return nil, translate(err)
|
|
}
|
|
var out []*domain.Claim
|
|
if err := cur.All(ctx, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// DeleteByItem removes an item's claims.
|
|
func (r *ClaimRepo) DeleteByItem(ctx context.Context, workspaceID, itemID string) error {
|
|
ctx, cancel := r.ctx(ctx)
|
|
defer cancel()
|
|
started := time.Now()
|
|
|
|
f, err := wsFilter(workspaceID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f = append(f, bson.E{Key: "item_id", Value: itemID})
|
|
|
|
_, err = r.coll.DeleteMany(ctx, f)
|
|
r.observe("delete_by_item", started, err)
|
|
return translate(err)
|
|
}
|
|
|
|
// Count returns the number of claims in a workspace.
|
|
func (r *ClaimRepo) Count(ctx context.Context, workspaceID string) (int64, error) {
|
|
ctx, cancel := r.ctx(ctx)
|
|
defer cancel()
|
|
started := time.Now()
|
|
|
|
f, err := wsFilter(workspaceID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
n, err := r.coll.CountDocuments(ctx, f)
|
|
r.observe("count", started, err)
|
|
return n, translate(err)
|
|
}
|