···
+
"Coves/internal/core/aggregators"
+
type postgresAggregatorRepo struct {
+
// NewAggregatorRepository creates a new PostgreSQL aggregator repository
+
func NewAggregatorRepository(db *sql.DB) aggregators.Repository {
+
return &postgresAggregatorRepo{db: db}
+
// ===== Aggregator CRUD Operations =====
+
// CreateAggregator indexes a new aggregator service declaration from the firehose
+
func (r *postgresAggregatorRepo) CreateAggregator(ctx context.Context, agg *aggregators.Aggregator) error {
+
INSERT INTO aggregators (
+
did, display_name, description, avatar_url, config_schema,
+
maintainer_did, source_url, created_at, indexed_at, record_uri, record_cid
+
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
+
ON CONFLICT (did) DO UPDATE SET
+
display_name = EXCLUDED.display_name,
+
description = EXCLUDED.description,
+
avatar_url = EXCLUDED.avatar_url,
+
config_schema = EXCLUDED.config_schema,
+
maintainer_did = EXCLUDED.maintainer_did,
+
source_url = EXCLUDED.source_url,
+
created_at = EXCLUDED.created_at,
+
indexed_at = EXCLUDED.indexed_at,
+
record_uri = EXCLUDED.record_uri,
+
record_cid = EXCLUDED.record_cid`
+
var configSchema interface{}
+
if len(agg.ConfigSchema) > 0 {
+
configSchema = agg.ConfigSchema
+
_, err := r.db.ExecContext(ctx, query,
+
nullString(agg.Description),
+
nullString(agg.AvatarURL),
+
nullString(agg.MaintainerDID),
+
nullString(agg.SourceURL),
+
nullString(agg.RecordURI),
+
nullString(agg.RecordCID),
+
return fmt.Errorf("failed to create aggregator: %w", err)
+
// GetAggregator retrieves an aggregator by DID
+
func (r *postgresAggregatorRepo) GetAggregator(ctx context.Context, did string) (*aggregators.Aggregator, error) {
+
did, display_name, description, avatar_url, config_schema,
+
maintainer_did, source_url, communities_using, posts_created,
+
created_at, indexed_at, record_uri, record_cid
+
agg := &aggregators.Aggregator{}
+
var description, avatarCID, maintainerDID, homepageURL, recordURI, recordCID sql.NullString
+
var configSchema []byte
+
err := r.db.QueryRowContext(ctx, query, did).Scan(
+
if err == sql.ErrNoRows {
+
return nil, aggregators.ErrAggregatorNotFound
+
return nil, fmt.Errorf("failed to get aggregator: %w", err)
+
agg.Description = description.String
+
agg.AvatarURL = avatarCID.String
+
agg.MaintainerDID = maintainerDID.String
+
agg.SourceURL = homepageURL.String
+
agg.RecordURI = recordURI.String
+
agg.RecordCID = recordCID.String
+
if configSchema != nil {
+
agg.ConfigSchema = configSchema
+
// GetAggregatorsByDIDs retrieves multiple aggregators by DIDs in a single query (avoids N+1)
+
func (r *postgresAggregatorRepo) GetAggregatorsByDIDs(ctx context.Context, dids []string) ([]*aggregators.Aggregator, error) {
+
return []*aggregators.Aggregator{}, nil
+
// Build IN clause with placeholders
+
placeholders := make([]string, len(dids))
+
args := make([]interface{}, len(dids))
+
for i, did := range dids {
+
placeholders[i] = fmt.Sprintf("$%d", i+1)
+
did, display_name, description, avatar_url, config_schema,
+
maintainer_did, source_url, communities_using, posts_created,
+
created_at, indexed_at, record_uri, record_cid
+
WHERE did IN (%s)`, strings.Join(placeholders, ", "))
+
rows, err := r.db.QueryContext(ctx, query, args...)
+
return nil, fmt.Errorf("failed to get aggregators: %w", err)
+
var results []*aggregators.Aggregator
+
agg := &aggregators.Aggregator{}
+
var description, avatarCID, maintainerDID, homepageURL, recordURI, recordCID sql.NullString
+
var configSchema []byte
+
return nil, fmt.Errorf("failed to scan aggregator: %w", err)
+
agg.Description = description.String
+
agg.AvatarURL = avatarCID.String
+
agg.MaintainerDID = maintainerDID.String
+
agg.SourceURL = homepageURL.String
+
agg.RecordURI = recordURI.String
+
agg.RecordCID = recordCID.String
+
if configSchema != nil {
+
agg.ConfigSchema = configSchema
+
results = append(results, agg)
+
if err = rows.Err(); err != nil {
+
return nil, fmt.Errorf("error iterating aggregators: %w", err)
+
// UpdateAggregator updates an existing aggregator
+
func (r *postgresAggregatorRepo) UpdateAggregator(ctx context.Context, agg *aggregators.Aggregator) error {
+
var configSchema interface{}
+
if len(agg.ConfigSchema) > 0 {
+
configSchema = agg.ConfigSchema
+
result, err := r.db.ExecContext(ctx, query,
+
nullString(agg.Description),
+
nullString(agg.AvatarURL),
+
nullString(agg.MaintainerDID),
+
nullString(agg.SourceURL),
+
nullString(agg.RecordURI),
+
nullString(agg.RecordCID),
+
return fmt.Errorf("failed to update aggregator: %w", err)
+
rows, err := result.RowsAffected()
+
return fmt.Errorf("failed to get rows affected: %w", err)
+
return aggregators.ErrAggregatorNotFound
+
// DeleteAggregator removes an aggregator (cascade deletes authorizations and posts via FK)
+
func (r *postgresAggregatorRepo) DeleteAggregator(ctx context.Context, did string) error {
+
query := `DELETE FROM aggregators WHERE did = $1`
+
result, err := r.db.ExecContext(ctx, query, did)
+
return fmt.Errorf("failed to delete aggregator: %w", err)
+
rows, err := result.RowsAffected()
+
return fmt.Errorf("failed to get rows affected: %w", err)
+
return aggregators.ErrAggregatorNotFound
+
// ListAggregators retrieves all aggregators with pagination
+
func (r *postgresAggregatorRepo) ListAggregators(ctx context.Context, limit, offset int) ([]*aggregators.Aggregator, error) {
+
did, display_name, description, avatar_url, config_schema,
+
maintainer_did, source_url, communities_using, posts_created,
+
created_at, indexed_at, record_uri, record_cid
+
ORDER BY communities_using DESC, display_name ASC
+
rows, err := r.db.QueryContext(ctx, query, limit, offset)
+
return nil, fmt.Errorf("failed to list aggregators: %w", err)
+
var aggs []*aggregators.Aggregator
+
agg := &aggregators.Aggregator{}
+
var description, avatarCID, maintainerDID, homepageURL, recordURI, recordCID sql.NullString
+
var configSchema []byte
+
return nil, fmt.Errorf("failed to scan aggregator: %w", err)
+
agg.Description = description.String
+
agg.AvatarURL = avatarCID.String
+
agg.MaintainerDID = maintainerDID.String
+
agg.SourceURL = homepageURL.String
+
agg.RecordURI = recordURI.String
+
agg.RecordCID = recordCID.String
+
if configSchema != nil {
+
agg.ConfigSchema = configSchema
+
aggs = append(aggs, agg)
+
if err = rows.Err(); err != nil {
+
return nil, fmt.Errorf("error iterating aggregators: %w", err)
+
// IsAggregator performs a fast existence check for post creation handler
+
func (r *postgresAggregatorRepo) IsAggregator(ctx context.Context, did string) (bool, error) {
+
query := `SELECT EXISTS(SELECT 1 FROM aggregators WHERE did = $1)`
+
err := r.db.QueryRowContext(ctx, query, did).Scan(&exists)
+
return false, fmt.Errorf("failed to check if aggregator exists: %w", err)
+
// ===== Authorization CRUD Operations =====
+
// CreateAuthorization indexes a new authorization from the firehose
+
func (r *postgresAggregatorRepo) CreateAuthorization(ctx context.Context, auth *aggregators.Authorization) error {
+
INSERT INTO aggregator_authorizations (
+
aggregator_did, community_did, enabled, config,
+
created_at, created_by, disabled_at, disabled_by,
+
indexed_at, record_uri, record_cid
+
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
+
ON CONFLICT (aggregator_did, community_did) DO UPDATE SET
+
enabled = EXCLUDED.enabled,
+
config = EXCLUDED.config,
+
created_at = EXCLUDED.created_at,
+
created_by = EXCLUDED.created_by,
+
disabled_at = EXCLUDED.disabled_at,
+
disabled_by = EXCLUDED.disabled_by,
+
indexed_at = EXCLUDED.indexed_at,
+
record_uri = EXCLUDED.record_uri,
+
record_cid = EXCLUDED.record_cid
+
if len(auth.Config) > 0 {
+
var disabledAt interface{}
+
if auth.DisabledAt != nil {
+
disabledAt = *auth.DisabledAt
+
err := r.db.QueryRowContext(ctx, query,
+
auth.CreatedBy, // Required field, no nullString needed
+
nullString(auth.DisabledBy),
+
nullString(auth.RecordURI),
+
nullString(auth.RecordCID),
+
// Check for foreign key violations
+
if strings.Contains(err.Error(), "fk_aggregator") {
+
return aggregators.ErrAggregatorNotFound
+
return fmt.Errorf("failed to create authorization: %w", err)
+
// GetAuthorization retrieves an authorization by aggregator and community DID
+
func (r *postgresAggregatorRepo) GetAuthorization(ctx context.Context, aggregatorDID, communityDID string) (*aggregators.Authorization, error) {
+
id, aggregator_did, community_did, enabled, config,
+
created_at, created_by, disabled_at, disabled_by,
+
indexed_at, record_uri, record_cid
+
FROM aggregator_authorizations
+
WHERE aggregator_did = $1 AND community_did = $2`
+
auth := &aggregators.Authorization{}
+
var createdBy, disabledBy, recordURI, recordCID sql.NullString
+
var disabledAt sql.NullTime
+
err := r.db.QueryRowContext(ctx, query, aggregatorDID, communityDID).Scan(
+
if err == sql.ErrNoRows {
+
return nil, aggregators.ErrAuthorizationNotFound
+
return nil, fmt.Errorf("failed to get authorization: %w", err)
+
auth.CreatedBy = createdBy.String
+
auth.DisabledBy = disabledBy.String
+
disabledAtVal := disabledAt.Time
+
auth.DisabledAt = &disabledAtVal
+
auth.RecordURI = recordURI.String
+
auth.RecordCID = recordCID.String
+
// GetAuthorizationByURI retrieves an authorization by record URI (for Jetstream delete operations)
+
func (r *postgresAggregatorRepo) GetAuthorizationByURI(ctx context.Context, recordURI string) (*aggregators.Authorization, error) {
+
id, aggregator_did, community_did, enabled, config,
+
created_at, created_by, disabled_at, disabled_by,
+
indexed_at, record_uri, record_cid
+
FROM aggregator_authorizations
+
auth := &aggregators.Authorization{}
+
var createdBy, disabledBy, recordURIField, recordCID sql.NullString
+
var disabledAt sql.NullTime
+
err := r.db.QueryRowContext(ctx, query, recordURI).Scan(
+
if err == sql.ErrNoRows {
+
return nil, aggregators.ErrAuthorizationNotFound
+
return nil, fmt.Errorf("failed to get authorization by URI: %w", err)
+
auth.CreatedBy = createdBy.String
+
auth.DisabledBy = disabledBy.String
+
disabledAtVal := disabledAt.Time
+
auth.DisabledAt = &disabledAtVal
+
auth.RecordURI = recordURIField.String
+
auth.RecordCID = recordCID.String
+
// UpdateAuthorization updates an existing authorization
+
func (r *postgresAggregatorRepo) UpdateAuthorization(ctx context.Context, auth *aggregators.Authorization) error {
+
UPDATE aggregator_authorizations SET
+
WHERE aggregator_did = $1 AND community_did = $2`
+
if len(auth.Config) > 0 {
+
var disabledAt interface{}
+
if auth.DisabledAt != nil {
+
disabledAt = *auth.DisabledAt
+
result, err := r.db.ExecContext(ctx, query,
+
nullString(auth.CreatedBy),
+
nullString(auth.DisabledBy),
+
nullString(auth.RecordURI),
+
nullString(auth.RecordCID),
+
return fmt.Errorf("failed to update authorization: %w", err)
+
rows, err := result.RowsAffected()
+
return fmt.Errorf("failed to get rows affected: %w", err)
+
return aggregators.ErrAuthorizationNotFound
+
// DeleteAuthorization removes an authorization
+
func (r *postgresAggregatorRepo) DeleteAuthorization(ctx context.Context, aggregatorDID, communityDID string) error {
+
query := `DELETE FROM aggregator_authorizations WHERE aggregator_did = $1 AND community_did = $2`
+
result, err := r.db.ExecContext(ctx, query, aggregatorDID, communityDID)
+
return fmt.Errorf("failed to delete authorization: %w", err)
+
rows, err := result.RowsAffected()
+
return fmt.Errorf("failed to get rows affected: %w", err)
+
return aggregators.ErrAuthorizationNotFound
+
// DeleteAuthorizationByURI removes an authorization by record URI (for Jetstream delete operations)
+
func (r *postgresAggregatorRepo) DeleteAuthorizationByURI(ctx context.Context, recordURI string) error {
+
query := `DELETE FROM aggregator_authorizations WHERE record_uri = $1`
+
result, err := r.db.ExecContext(ctx, query, recordURI)
+
return fmt.Errorf("failed to delete authorization by URI: %w", err)
+
rows, err := result.RowsAffected()
+
return fmt.Errorf("failed to get rows affected: %w", err)
+
return aggregators.ErrAuthorizationNotFound
+
// ===== Authorization Query Operations =====
+
// ListAuthorizationsForAggregator retrieves all communities that authorized an aggregator
+
func (r *postgresAggregatorRepo) ListAuthorizationsForAggregator(ctx context.Context, aggregatorDID string, enabledOnly bool, limit, offset int) ([]*aggregators.Authorization, error) {
+
id, aggregator_did, community_did, enabled, config,
+
created_at, created_by, disabled_at, disabled_by,
+
indexed_at, record_uri, record_cid
+
FROM aggregator_authorizations
+
WHERE aggregator_did = $1`
+
query = baseQuery + ` AND enabled = true ORDER BY created_at DESC LIMIT $2 OFFSET $3`
+
args = []interface{}{aggregatorDID, limit, offset}
+
query = baseQuery + ` ORDER BY created_at DESC LIMIT $2 OFFSET $3`
+
args = []interface{}{aggregatorDID, limit, offset}
+
rows, err := r.db.QueryContext(ctx, query, args...)
+
return nil, fmt.Errorf("failed to list authorizations for aggregator: %w", err)
+
return scanAuthorizations(rows)
+
// ListAuthorizationsForCommunity retrieves all aggregators authorized by a community
+
func (r *postgresAggregatorRepo) ListAuthorizationsForCommunity(ctx context.Context, communityDID string, enabledOnly bool, limit, offset int) ([]*aggregators.Authorization, error) {
+
id, aggregator_did, community_did, enabled, config,
+
created_at, created_by, disabled_at, disabled_by,
+
indexed_at, record_uri, record_cid
+
FROM aggregator_authorizations
+
WHERE community_did = $1`
+
query = baseQuery + ` AND enabled = true ORDER BY created_at DESC LIMIT $2 OFFSET $3`
+
args = []interface{}{communityDID, limit, offset}
+
query = baseQuery + ` ORDER BY created_at DESC LIMIT $2 OFFSET $3`
+
args = []interface{}{communityDID, limit, offset}
+
rows, err := r.db.QueryContext(ctx, query, args...)
+
return nil, fmt.Errorf("failed to list authorizations for community: %w", err)
+
return scanAuthorizations(rows)
+
// IsAuthorized performs a fast authorization check (enabled=true)
+
// Uses the optimized partial index: idx_aggregator_auth_enabled
+
func (r *postgresAggregatorRepo) IsAuthorized(ctx context.Context, aggregatorDID, communityDID string) (bool, error) {
+
SELECT 1 FROM aggregator_authorizations
+
WHERE aggregator_did = $1 AND community_did = $2 AND enabled = true
+
err := r.db.QueryRowContext(ctx, query, aggregatorDID, communityDID).Scan(&authorized)
+
return false, fmt.Errorf("failed to check authorization: %w", err)
+
// ===== Post Tracking Operations =====
+
// RecordAggregatorPost tracks a post created by an aggregator (for rate limiting and stats)
+
func (r *postgresAggregatorRepo) RecordAggregatorPost(ctx context.Context, aggregatorDID, communityDID, postURI, postCID string) error {
+
INSERT INTO aggregator_posts (aggregator_did, community_did, post_uri, post_cid, created_at)
+
VALUES ($1, $2, $3, $4, NOW())`
+
_, err := r.db.ExecContext(ctx, query, aggregatorDID, communityDID, postURI, postCID)
+
return fmt.Errorf("failed to record aggregator post: %w", err)
+
// CountRecentPosts counts posts created by an aggregator in a community since a given time
+
// Uses the optimized index: idx_aggregator_posts_rate_limit
+
func (r *postgresAggregatorRepo) CountRecentPosts(ctx context.Context, aggregatorDID, communityDID string, since time.Time) (int, error) {
+
WHERE aggregator_did = $1 AND community_did = $2 AND created_at >= $3`
+
err := r.db.QueryRowContext(ctx, query, aggregatorDID, communityDID, since).Scan(&count)
+
return 0, fmt.Errorf("failed to count recent posts: %w", err)
+
// GetRecentPosts retrieves recent posts created by an aggregator in a community
+
func (r *postgresAggregatorRepo) GetRecentPosts(ctx context.Context, aggregatorDID, communityDID string, since time.Time) ([]*aggregators.AggregatorPost, error) {
+
SELECT id, aggregator_did, community_did, post_uri, created_at
+
WHERE aggregator_did = $1 AND community_did = $2 AND created_at >= $3
+
ORDER BY created_at DESC`
+
rows, err := r.db.QueryContext(ctx, query, aggregatorDID, communityDID, since)
+
return nil, fmt.Errorf("failed to get recent posts: %w", err)
+
var posts []*aggregators.AggregatorPost
+
post := &aggregators.AggregatorPost{}
+
return nil, fmt.Errorf("failed to scan aggregator post: %w", err)
+
posts = append(posts, post)
+
if err = rows.Err(); err != nil {
+
return nil, fmt.Errorf("error iterating aggregator posts: %w", err)
+
// ===== Helper Functions =====
+
// scanAuthorizations is a helper to scan multiple authorization rows
+
func scanAuthorizations(rows *sql.Rows) ([]*aggregators.Authorization, error) {
+
var auths []*aggregators.Authorization
+
auth := &aggregators.Authorization{}
+
var createdBy, disabledBy, recordURI, recordCID sql.NullString
+
var disabledAt sql.NullTime
+
return nil, fmt.Errorf("failed to scan authorization: %w", err)
+
auth.CreatedBy = createdBy.String
+
auth.DisabledBy = disabledBy.String
+
disabledAtVal := disabledAt.Time
+
auth.DisabledAt = &disabledAtVal
+
auth.RecordURI = recordURI.String
+
auth.RecordCID = recordCID.String
+
auths = append(auths, auth)
+
if err := rows.Err(); err != nil {
+
return nil, fmt.Errorf("error iterating authorizations: %w", err)