Files
meta-core/internal/storage/udl.go
T

187 lines
5.0 KiB
Go

package storage
// The User Data Layer facade (/api/udl/*). Since v2.0.0 a user's data are
// ordinary claims signed by the user's key — plaintext for public-social keys
// (like, userRating, profile:*), ciphertext for private ones (seek, watched,
// …). There is no separate store: these helpers read the claims of one signer
// through the cache's claim_by_pid index and the truth claim table.
import (
"database/sql"
"errors"
"sort"
"github.com/metazla/meta-core/internal/claim"
"github.com/metazla/meta-core/internal/identity"
)
// UDLUserStats summarises one signer's live claims.
type UDLUserStats struct {
Records int `json:"records"`
Cids int `json:"cids"`
Keys int `json:"keys"`
}
// UDLGetClaim returns the stored claim of uid on (cid, key), tombstone
// included (a client needs its ts to write a newer one). ok is false when the
// signer never voted on it.
func (c *Client) UDLGetClaim(uid, cid, key string) (claim.Claim, bool, error) {
if _, err := c.truthStore(); err != nil {
return claim.Claim{}, false, err
}
ctx, cancel := opCtx()
defer cancel()
cl := claim.Claim{CID: cid, UID: uid, Key: key}
var raw any
var ts int64
err := c.truth.DB().QueryRowContext(ctx,
`SELECT c.value, c.ts, c.sig FROM claim c JOIN principal p ON p.pid = c.pid
WHERE c.cid = ? AND c.key = ? AND p.uid = ?`, cid, key, uid).Scan(&raw, &ts, &cl.Sig)
if errors.Is(err, sql.ErrNoRows) {
return claim.Claim{}, false, nil
}
if err != nil {
return claim.Claim{}, false, err
}
cl.Value, cl.TS = scanValue(raw), uint64(ts)
return cl, true, nil
}
// UDLClaimsForUser returns uid's live claims, optionally narrowed to one cid
// and/or one key, ordered by cid then key.
func (c *Client) UDLClaimsForUser(uid, cid, key string) ([]claim.Claim, error) {
refs, err := c.LiveClaimsBySigner(uid)
if err != nil {
return nil, err
}
var out []claim.Claim
for _, r := range refs {
if (cid != "" && r[0] != cid) || (key != "" && r[1] != key) {
continue
}
cl, ok, err := c.UDLGetClaim(uid, r[0], r[1])
if err != nil {
return nil, err
}
if ok && !cl.Value.IsNull() {
out = append(out, cl)
}
}
return out, nil
}
// UDLAllForUser pages uid's live claims by cid: claims of cids strictly after
// `after`, stopping at a cid boundary once limit claims are collected.
// nextCid is "" when the walk is complete.
func (c *Client) UDLAllForUser(uid, after string, limit int) ([]claim.Claim, string, error) {
all, err := c.UDLClaimsForUser(uid, "", "")
if err != nil {
return nil, "", err
}
var out []claim.Claim
for _, cl := range all {
if after != "" && cl.CID <= after {
continue
}
if len(out) >= limit && cl.CID != out[len(out)-1].CID {
return out, out[len(out)-1].CID, nil
}
out = append(out, cl)
}
return out, "", nil
}
// UDLCountUserCids counts the distinct cids uid holds live claims on.
func (c *Client) UDLCountUserCids(uid string) (int, error) {
refs, err := c.LiveClaimsBySigner(uid)
if err != nil {
return 0, err
}
seen := map[string]bool{}
for _, r := range refs {
seen[r[0]] = true
}
return len(seen), nil
}
// UDLCidUsers lists the signers holding live claims on cid.
func (c *Client) UDLCidUsers(cid string) ([]string, error) {
claims, err := c.ClaimsForCIDs([]string{cid}, "")
if err != nil {
return nil, err
}
seen := map[string]bool{}
out := []string{}
for _, cl := range claims {
if !seen[cl.UID] {
seen[cl.UID] = true
out = append(out, cl.UID)
}
}
sort.Strings(out)
return out, nil
}
// UDLPublicValues returns the plaintext values of every live claim on
// (cid, key) — the input of /api/udl/cid/{cid}/aggregate.
func (c *Client) UDLPublicValues(cid, key string) ([]string, error) {
claims, err := c.ClaimsForCIDs([]string{cid}, key)
if err != nil {
return nil, err
}
out := []string{}
for _, cl := range claims {
if cl.Value.Kind == claim.KindText {
out = append(out, cl.Value.Text)
}
}
return out, nil
}
// UDLAllUserStats summarises every signer that holds live claims, keyed by
// uid. Callers that want only human accounts intersect it with the account
// roster.
func (c *Client) UDLAllUserStats() (map[string]UDLUserStats, error) {
s, err := c.kv()
if err != nil {
return nil, err
}
if _, err := c.truthStore(); err != nil {
return nil, err
}
ctx, cancel := opCtx()
defer cancel()
rows, err := s.DB().QueryContext(ctx,
`SELECT pid, count(*), count(DISTINCT cid), count(DISTINCT key) FROM claim_by_pid GROUP BY pid`)
if err != nil {
return nil, err
}
type agg struct {
pid int64
st UDLUserStats
}
var aggs []agg
for rows.Next() {
var a agg
if err := rows.Scan(&a.pid, &a.st.Records, &a.st.Cids, &a.st.Keys); err != nil {
rows.Close()
return nil, err
}
aggs = append(aggs, a)
}
if err := rows.Close(); err != nil {
return nil, err
}
out := make(map[string]UDLUserStats, len(aggs))
for _, a := range aggs {
var uid string
if err := c.truth.DB().QueryRowContext(ctx, `SELECT uid FROM principal WHERE pid = ?`, a.pid).Scan(&uid); err != nil {
continue
}
if identity.ValidUID(uid) {
out[uid] = a.st
}
}
return out, nil
}