Skip to main content

keepsake_sqlx/repository/
query.rs

1use std::collections::BTreeMap;
2
3use keepsake::{
4    ActiveRelation, ActiveRelationSource, Keepsake, RelationDefinition, RelationId, RelationKey,
5    SubjectRef,
6};
7
8use super::{
9    ActiveRelationRow, AppliedKeepsakeRow, KeepsakeRepository, MembershipCursor, RelationCache,
10    RepositoryError, RepositoryResult, validate_limit,
11};
12
13impl<C> KeepsakeRepository<C>
14where
15    C: RelationCache,
16{
17    /// Returns active keepsakes for a subject.
18    pub async fn active_for_subject(
19        &self,
20        subject: &SubjectRef,
21    ) -> RepositoryResult<Vec<Keepsake>> {
22        let rows = sqlx::query_as::<_, AppliedKeepsakeRow>(
23            r"
24            select id, subject_kind, subject_id, relation_id, state, expiry_policy, applied_at,
25                expires_at, fulfilled_at, revoked_at, metadata
26            from keepsakes
27            where subject_kind = $1 and subject_id = $2 and state = 'applied'
28            order by relation_id, id
29            ",
30        )
31        .bind(subject.kind())
32        .bind(subject.id())
33        .fetch_all(&self.pool)
34        .await?;
35
36        rows.into_iter()
37            .map(AppliedKeepsakeRow::try_into_keepsake)
38            .collect()
39    }
40
41    /// Returns active keepsakes for a subject with their relation definitions.
42    pub async fn active_relations_for_subject(
43        &self,
44        subject: &SubjectRef,
45    ) -> RepositoryResult<Vec<ActiveRelation>> {
46        let rows = sqlx::query_as::<_, ActiveRelationRow>(
47            r"
48            select
49                k.id,
50                k.subject_kind,
51                k.subject_id,
52                k.relation_id,
53                k.state,
54                k.expiry_policy,
55                k.applied_at,
56                k.expires_at,
57                k.fulfilled_at,
58                k.revoked_at,
59                k.metadata,
60                r.id as relation_definition_id,
61                r.kind as relation_kind,
62                r.key as relation_key,
63                r.enabled as relation_enabled,
64                r.expiry_policy as relation_expiry_policy
65            from keepsakes k
66            join keepsake_relation_definitions r on r.id = k.relation_id
67            where k.subject_kind = $1 and k.subject_id = $2 and k.state = 'applied'
68            order by k.relation_id, k.id
69            ",
70        )
71        .bind(subject.kind())
72        .bind(subject.id())
73        .fetch_all(&self.pool)
74        .await?;
75
76        let mut active = Vec::with_capacity(rows.len());
77        for row in rows {
78            let active_relation = row.try_into_active_relation()?;
79            self.relation_cache.store(active_relation.relation()).await;
80            active.push(active_relation);
81        }
82        Ok(active)
83    }
84
85    /// Returns active keepsakes for a subject, filtered by relation ids.
86    ///
87    /// This is the bounded variant of [`Self::active_relations_for_subject`] for
88    /// request paths that use typed relation specs or another stable relation-id
89    /// catalogue. Missing ids are ignored, duplicate requested ids do not
90    /// duplicate output rows, and disabled relation definitions are still
91    /// returned when their keepsake is active.
92    pub async fn active_relations_for_subject_by_ids(
93        &self,
94        subject: &SubjectRef,
95        relation_ids: &[RelationId],
96    ) -> RepositoryResult<Vec<ActiveRelation>> {
97        if relation_ids.is_empty() {
98            return Ok(Vec::new());
99        }
100
101        let cached_relations = self.cached_relations_by_ids(relation_ids).await;
102        if let Some(relations_by_id) = cached_relations {
103            let requested_relation_ids = relations_by_id.keys().copied().collect::<Vec<_>>();
104            let keepsakes = self
105                .active_for_subject_keepsakes_by_ids(subject, &requested_relation_ids)
106                .await?;
107            return active_relations_from_keepsakes(keepsakes, &relations_by_id);
108        }
109
110        let requested_relation_ids = relation_ids.to_vec();
111        let rows = sqlx::query_as::<_, ActiveRelationRow>(
112            r"
113            with requested_relation_ids(id) as (
114                select distinct id
115                from unnest($3::uuid[]) as requested(id)
116            )
117            select
118                k.id,
119                k.subject_kind,
120                k.subject_id,
121                k.relation_id,
122                k.state,
123                k.expiry_policy,
124                k.applied_at,
125                k.expires_at,
126                k.fulfilled_at,
127                k.revoked_at,
128                k.metadata,
129                r.id as relation_definition_id,
130                r.kind as relation_kind,
131                r.key as relation_key,
132                r.enabled as relation_enabled,
133                r.expiry_policy as relation_expiry_policy
134            from requested_relation_ids requested
135            join keepsake_relation_definitions r
136              on r.id = requested.id
137            join keepsakes k
138              on k.relation_id = r.id
139             and k.subject_kind = $1
140             and k.subject_id = $2
141             and k.state = 'applied'
142            order by k.relation_id, k.id
143            ",
144        )
145        .bind(subject.kind())
146        .bind(subject.id())
147        .bind(&requested_relation_ids)
148        .fetch_all(&self.pool)
149        .await?;
150
151        let mut active = Vec::with_capacity(rows.len());
152        for row in rows {
153            let active_relation = row.try_into_active_relation()?;
154            self.relation_cache.store(active_relation.relation()).await;
155            active.push(active_relation);
156        }
157        Ok(active)
158    }
159
160    /// Returns active keepsakes for a subject, filtered by relation keys.
161    ///
162    /// This is the bounded variant of [`Self::active_relations_for_subject`] for
163    /// request paths that know the small set of relation keys they care about.
164    /// Missing keys are ignored, and disabled relation definitions are still
165    /// returned when their keepsake is active.
166    pub async fn active_relations_for_subject_by_keys(
167        &self,
168        subject: &SubjectRef,
169        keys: &[RelationKey],
170    ) -> RepositoryResult<Vec<ActiveRelation>> {
171        if keys.is_empty() {
172            return Ok(Vec::new());
173        }
174
175        let cached_relations = self.cached_relations_by_keys(keys).await;
176        if let Some(relations_by_id) = cached_relations {
177            let requested_relation_ids = relations_by_id.keys().copied().collect::<Vec<_>>();
178            let keepsakes = self
179                .active_for_subject_keepsakes_by_ids(subject, &requested_relation_ids)
180                .await?;
181            return active_relations_from_keepsakes(keepsakes, &relations_by_id);
182        }
183
184        let kinds = keys
185            .iter()
186            .map(|key| key.kind().to_owned())
187            .collect::<Vec<String>>();
188        let names = keys
189            .iter()
190            .map(|key| key.name().to_owned())
191            .collect::<Vec<String>>();
192
193        let rows = sqlx::query_as::<_, ActiveRelationRow>(
194            r"
195            with requested_relation_keys(kind, key) as (
196                select distinct kind, key
197                from unnest($3::text[], $4::text[]) as requested(kind, key)
198            )
199            select
200                k.id,
201                k.subject_kind,
202                k.subject_id,
203                k.relation_id,
204                k.state,
205                k.expiry_policy,
206                k.applied_at,
207                k.expires_at,
208                k.fulfilled_at,
209                k.revoked_at,
210                k.metadata,
211                r.id as relation_definition_id,
212                r.kind as relation_kind,
213                r.key as relation_key,
214                r.enabled as relation_enabled,
215                r.expiry_policy as relation_expiry_policy
216            from requested_relation_keys requested
217            join keepsake_relation_definitions r
218              on r.kind = requested.kind and r.key = requested.key
219            join keepsakes k
220              on k.relation_id = r.id
221             and k.subject_kind = $1
222             and k.subject_id = $2
223             and k.state = 'applied'
224            order by k.relation_id, k.id
225            ",
226        )
227        .bind(subject.kind())
228        .bind(subject.id())
229        .bind(&kinds)
230        .bind(&names)
231        .fetch_all(&self.pool)
232        .await?;
233
234        let mut active = Vec::with_capacity(rows.len());
235        for row in rows {
236            let active_relation = row.try_into_active_relation()?;
237            self.relation_cache.store(active_relation.relation()).await;
238            active.push(active_relation);
239        }
240        Ok(active)
241    }
242
243    /// Scans active memberships for a relation in stable order.
244    pub async fn active_membership_scan(
245        &self,
246        relation_id: RelationId,
247        limit: i64,
248    ) -> RepositoryResult<Vec<Keepsake>> {
249        self.active_membership_scan_after(relation_id, None, limit)
250            .await
251    }
252
253    /// Scans active memberships after a keyset cursor in stable order.
254    pub async fn active_membership_scan_after(
255        &self,
256        relation_id: RelationId,
257        after: Option<&MembershipCursor>,
258        limit: i64,
259    ) -> RepositoryResult<Vec<Keepsake>> {
260        let limit = validate_limit(limit)?;
261        let rows = sqlx::query_as::<_, AppliedKeepsakeRow>(
262            r"
263            select id, subject_kind, subject_id, relation_id, state, expiry_policy, applied_at,
264                expires_at, fulfilled_at, revoked_at, metadata
265            from keepsakes
266            where relation_id = $1
267              and state = 'applied'
268              and (
269                $2::text is null
270                or (subject_kind, subject_id, id) > ($2, $3, $4)
271              )
272            order by subject_kind, subject_id, id
273            limit $5
274            ",
275        )
276        .bind(relation_id)
277        .bind(after.map(|cursor| cursor.subject_kind.as_str()))
278        .bind(after.map(|cursor| cursor.subject_id.as_str()))
279        .bind(after.map(|cursor| cursor.keepsake_id))
280        .bind(limit)
281        .fetch_all(&self.pool)
282        .await?;
283
284        rows.into_iter()
285            .map(AppliedKeepsakeRow::try_into_keepsake)
286            .collect()
287    }
288
289    async fn cached_relations_by_ids(
290        &self,
291        relation_ids: &[RelationId],
292    ) -> Option<BTreeMap<RelationId, RelationDefinition>> {
293        let mut relations = BTreeMap::new();
294        for relation_id in relation_ids {
295            let relation = self.relation_cache.get_by_id(*relation_id).await?;
296            relations.insert(relation.id, relation);
297        }
298        Some(relations)
299    }
300
301    async fn cached_relations_by_keys(
302        &self,
303        keys: &[RelationKey],
304    ) -> Option<BTreeMap<RelationId, RelationDefinition>> {
305        let mut relations = BTreeMap::new();
306        for key in keys {
307            let relation = self.relation_cache.get_by_key(key).await?;
308            relations.insert(relation.id, relation);
309        }
310        Some(relations)
311    }
312
313    async fn active_for_subject_keepsakes_by_ids(
314        &self,
315        subject: &SubjectRef,
316        relation_ids: &[RelationId],
317    ) -> RepositoryResult<Vec<Keepsake>> {
318        let rows = sqlx::query_as::<_, AppliedKeepsakeRow>(
319            r"
320            with requested_relation_ids(id) as (
321                select distinct id
322                from unnest($3::uuid[]) as requested(id)
323            )
324            select k.id, k.subject_kind, k.subject_id, k.relation_id, k.state, k.expiry_policy,
325                k.applied_at, k.expires_at, k.fulfilled_at, k.revoked_at, k.metadata
326            from requested_relation_ids requested
327            join keepsakes k
328              on k.relation_id = requested.id
329             and k.subject_kind = $1
330             and k.subject_id = $2
331             and k.state = 'applied'
332            order by k.relation_id, k.id
333            ",
334        )
335        .bind(subject.kind())
336        .bind(subject.id())
337        .bind(relation_ids)
338        .fetch_all(&self.pool)
339        .await?;
340
341        rows.into_iter()
342            .map(AppliedKeepsakeRow::try_into_keepsake)
343            .collect()
344    }
345}
346
347fn active_relations_from_keepsakes(
348    keepsakes: Vec<Keepsake>,
349    relations_by_id: &BTreeMap<RelationId, RelationDefinition>,
350) -> RepositoryResult<Vec<ActiveRelation>> {
351    keepsakes
352        .into_iter()
353        .map(|keepsake| {
354            let relation = relations_by_id
355                .get(&keepsake.relation_id())
356                .cloned()
357                .ok_or(RepositoryError::RelationDefinitionMissing {
358                    relation_id: keepsake.relation_id(),
359                })?;
360            Ok(ActiveRelation::new(keepsake, relation)?)
361        })
362        .collect()
363}
364
365impl<C> ActiveRelationSource for KeepsakeRepository<C>
366where
367    C: RelationCache,
368{
369    type Error = RepositoryError;
370
371    async fn active_relations_for_subject<'a>(
372        &'a self,
373        subject: &'a SubjectRef,
374    ) -> RepositoryResult<Vec<ActiveRelation>> {
375        self.active_relations_for_subject(subject).await
376    }
377
378    async fn active_relations_for_subject_by_ids<'a>(
379        &'a self,
380        subject: &'a SubjectRef,
381        relation_ids: &'a [RelationId],
382    ) -> RepositoryResult<Vec<ActiveRelation>> {
383        self.active_relations_for_subject_by_ids(subject, relation_ids)
384            .await
385    }
386
387    async fn active_relations_for_subject_by_keys<'a>(
388        &'a self,
389        subject: &'a SubjectRef,
390        keys: &'a [RelationKey],
391    ) -> RepositoryResult<Vec<ActiveRelation>> {
392        self.active_relations_for_subject_by_keys(subject, keys)
393            .await
394    }
395}