Files
etalon f2c534f636 feat: store layer with indexes, optimistic concurrency, GridFS, credential crypto (phase 2)
- mongo-driver v2 connection with bounded startup retry + /readyz gate
- idempotent creation of all §4.9 indexes
- UpdateVersioned: version-filtered updates, conflict vs not-found errors
- AES-256-GCM Seal/Open for ticketing credentials, base64(nonce|ct)
- GridFS file store: MIME sniffing, MAX_UPLOAD_MB cap, sha256 metadata
- HMAC-signed short-lived file URL tokens (§5.1, 1h TTL)
- integration tests against compose Mongo via docker-compose.test.yml overlay

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:17:26 +02:00

76 lines
2.2 KiB
Go

package store
import (
"context"
"errors"
"fmt"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
)
// Optimistic-concurrency errors. HTTP handlers translate ErrVersionConflict
// into a 409 with a "conflict" envelope (and the QoL reload toast).
var (
ErrNotFound = errors.New("document not found")
ErrVersionConflict = errors.New("version conflict")
)
// UpdateVersioned applies update to the document only if its version still
// matches, bumping version and updatedAt atomically. update uses normal
// operators ($set/$push/...); the helper merges its bookkeeping into them.
func UpdateVersioned(ctx context.Context, coll *mongo.Collection, id string, version int64, update bson.M) error {
merged := bson.M{}
for op, fields := range update {
merged[op] = fields
}
set, _ := merged["$set"].(bson.M)
if set == nil {
set = bson.M{}
}
set["updatedAt"] = time.Now().UTC()
merged["$set"] = set
inc, _ := merged["$inc"].(bson.M)
if inc == nil {
inc = bson.M{}
}
if _, clash := inc["version"]; clash {
return fmt.Errorf("update must not touch version itself")
}
inc["version"] = int64(1)
merged["$inc"] = inc
res, err := coll.UpdateOne(ctx, bson.M{"_id": id, "version": version}, merged)
if err != nil {
return fmt.Errorf("versioned update %s/%s: %w", coll.Name(), id, err)
}
if res.MatchedCount == 1 {
return nil
}
// Distinguish "gone" from "someone got there first".
n, err := coll.CountDocuments(ctx, bson.M{"_id": id})
if err != nil {
return fmt.Errorf("versioned update %s/%s: existence check: %w", coll.Name(), id, err)
}
if n == 0 {
return fmt.Errorf("%s/%s: %w", coll.Name(), id, ErrNotFound)
}
return fmt.Errorf("%s/%s at version %d: %w", coll.Name(), id, version, ErrVersionConflict)
}
// InsertOne inserts a document map, stamping createdAt/updatedAt/version so
// every collection follows the §4 conventions.
func InsertOne(ctx context.Context, coll *mongo.Collection, id string, doc bson.M) error {
now := time.Now().UTC()
doc["_id"] = id
doc["createdAt"] = now
doc["updatedAt"] = now
doc["version"] = int64(1)
if _, err := coll.InsertOne(ctx, doc); err != nil {
return fmt.Errorf("insert %s/%s: %w", coll.Name(), id, err)
}
return nil
}