Code sync, security fixes
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user