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>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user