f2c534f636
- 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>
76 lines
2.2 KiB
Go
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
|
|
}
|