Skip to main content

keepsake_sqlx/repository/
query.rs

1use keepsake::{Keepsake, RelationId, RelationKey, SubjectRef};
2
3use super::{
4    ActiveRelation, ActiveRelationRow, AppliedKeepsakeRow, KeepsakeRepository, MembershipCursor,
5    RelationCache, RepositoryResult, validate_limit,
6};
7
8impl<C> KeepsakeRepository<C>
9where
10    C: RelationCache,
11{
12    /// Returns active keepsakes for a subject.
13    pub async fn active_for_subject(
14        &self,
15        subject: &SubjectRef,
16    ) -> RepositoryResult<Vec<Keepsake>> {
17        let rows = sqlx::query_as::<_, AppliedKeepsakeRow>(
18            r"
19            select id, subject_kind, subject_id, relation_id, state, expiry_policy, applied_at,
20                expires_at, fulfilled_at, revoked_at, metadata
21            from keepsakes
22            where subject_kind = $1 and subject_id = $2 and state = 'applied'
23            order by relation_id, id
24            ",
25        )
26        .bind(&subject.kind)
27        .bind(&subject.id)
28        .fetch_all(&self.pool)
29        .await?;
30
31        rows.into_iter()
32            .map(AppliedKeepsakeRow::try_into_keepsake)
33            .collect()
34    }
35
36    /// Returns active keepsakes for a subject with their relation definitions.
37    pub async fn active_relations_for_subject(
38        &self,
39        subject: &SubjectRef,
40    ) -> RepositoryResult<Vec<ActiveRelation>> {
41        let rows = sqlx::query_as::<_, ActiveRelationRow>(
42            r"
43            select
44                k.id,
45                k.subject_kind,
46                k.subject_id,
47                k.relation_id,
48                k.state,
49                k.expiry_policy,
50                k.applied_at,
51                k.expires_at,
52                k.fulfilled_at,
53                k.revoked_at,
54                k.metadata,
55                r.id as relation_definition_id,
56                r.kind as relation_kind,
57                r.key as relation_key,
58                r.enabled as relation_enabled,
59                r.expiry_policy as relation_expiry_policy
60            from keepsakes k
61            join keepsake_relation_definitions r on r.id = k.relation_id
62            where k.subject_kind = $1 and k.subject_id = $2 and k.state = 'applied'
63            order by k.relation_id, k.id
64            ",
65        )
66        .bind(&subject.kind)
67        .bind(&subject.id)
68        .fetch_all(&self.pool)
69        .await?;
70
71        let mut active = Vec::with_capacity(rows.len());
72        for row in rows {
73            let active_relation = row.try_into_active_relation()?;
74            self.relation_cache.store(&active_relation.relation).await;
75            active.push(active_relation);
76        }
77        Ok(active)
78    }
79
80    /// Returns active keepsakes for a subject, filtered by relation keys.
81    ///
82    /// This is the bounded variant of [`Self::active_relations_for_subject`] for
83    /// request paths that know the small set of relation keys they care about.
84    /// Missing keys are ignored, and disabled relation definitions are still
85    /// returned when their keepsake is active.
86    pub async fn active_relations_for_subject_by_keys(
87        &self,
88        subject: &SubjectRef,
89        keys: &[RelationKey],
90    ) -> RepositoryResult<Vec<ActiveRelation>> {
91        if keys.is_empty() {
92            return Ok(Vec::new());
93        }
94
95        let kinds = keys
96            .iter()
97            .map(|key| key.kind().to_owned())
98            .collect::<Vec<String>>();
99        let names = keys
100            .iter()
101            .map(|key| key.name().to_owned())
102            .collect::<Vec<String>>();
103
104        let rows = sqlx::query_as::<_, ActiveRelationRow>(
105            r"
106            with requested_relation_keys(kind, key) as (
107                select distinct kind, key
108                from unnest($3::text[], $4::text[]) as requested(kind, key)
109            )
110            select
111                k.id,
112                k.subject_kind,
113                k.subject_id,
114                k.relation_id,
115                k.state,
116                k.expiry_policy,
117                k.applied_at,
118                k.expires_at,
119                k.fulfilled_at,
120                k.revoked_at,
121                k.metadata,
122                r.id as relation_definition_id,
123                r.kind as relation_kind,
124                r.key as relation_key,
125                r.enabled as relation_enabled,
126                r.expiry_policy as relation_expiry_policy
127            from requested_relation_keys requested
128            join keepsake_relation_definitions r
129              on r.kind = requested.kind and r.key = requested.key
130            join keepsakes k
131              on k.relation_id = r.id
132             and k.subject_kind = $1
133             and k.subject_id = $2
134             and k.state = 'applied'
135            order by k.relation_id, k.id
136            ",
137        )
138        .bind(&subject.kind)
139        .bind(&subject.id)
140        .bind(&kinds)
141        .bind(&names)
142        .fetch_all(&self.pool)
143        .await?;
144
145        let mut active = Vec::with_capacity(rows.len());
146        for row in rows {
147            let active_relation = row.try_into_active_relation()?;
148            self.relation_cache.store(&active_relation.relation).await;
149            active.push(active_relation);
150        }
151        Ok(active)
152    }
153
154    /// Scans active memberships for a relation in stable order.
155    pub async fn active_membership_scan(
156        &self,
157        relation_id: RelationId,
158        limit: i64,
159    ) -> RepositoryResult<Vec<Keepsake>> {
160        self.active_membership_scan_after(relation_id, None, limit)
161            .await
162    }
163
164    /// Scans active memberships after a keyset cursor in stable order.
165    pub async fn active_membership_scan_after(
166        &self,
167        relation_id: RelationId,
168        after: Option<&MembershipCursor>,
169        limit: i64,
170    ) -> RepositoryResult<Vec<Keepsake>> {
171        let limit = validate_limit(limit)?;
172        let rows = sqlx::query_as::<_, AppliedKeepsakeRow>(
173            r"
174            select id, subject_kind, subject_id, relation_id, state, expiry_policy, applied_at,
175                expires_at, fulfilled_at, revoked_at, metadata
176            from keepsakes
177            where relation_id = $1
178              and state = 'applied'
179              and (
180                $2::text is null
181                or (subject_kind, subject_id, id) > ($2, $3, $4)
182              )
183            order by subject_kind, subject_id, id
184            limit $5
185            ",
186        )
187        .bind(relation_id)
188        .bind(after.map(|cursor| cursor.subject_kind.as_str()))
189        .bind(after.map(|cursor| cursor.subject_id.as_str()))
190        .bind(after.map(|cursor| cursor.keepsake_id))
191        .bind(limit)
192        .fetch_all(&self.pool)
193        .await?;
194
195        rows.into_iter()
196            .map(AppliedKeepsakeRow::try_into_keepsake)
197            .collect()
198    }
199}