Skip to main content

cratestack_sqlx/
delegate.rs

1use crate::sqlx;
2
3use cratestack_core::{CoolContext, CoolError};
4
5use crate::{
6    BatchCreate, BatchDelete, BatchGet, BatchUpdate, BatchUpdateItem, BatchUpsert,
7    CreateModelInput, CreateRecord, DeleteRecord, Filter, FilterExpr, FindMany, FindUnique,
8    ModelDescriptor, OrderClause, SqlxRuntime, UpdateModelInput, UpdateRecord, UpdateRecordSet,
9    UpsertModelInput, UpsertRecord,
10};
11
12#[derive(Debug, Clone, Copy)]
13pub struct ModelDelegate<'a, M: 'static, PK: 'static> {
14    runtime: &'a SqlxRuntime,
15    descriptor: &'static ModelDescriptor<M, PK>,
16}
17
18impl<'a, M: 'static, PK: 'static> ModelDelegate<'a, M, PK> {
19    pub fn new(runtime: &'a SqlxRuntime, descriptor: &'static ModelDescriptor<M, PK>) -> Self {
20        Self {
21            runtime,
22            descriptor,
23        }
24    }
25
26    pub fn descriptor(&self) -> &'static ModelDescriptor<M, PK> {
27        self.descriptor
28    }
29
30    pub fn bind(self, ctx: CoolContext) -> ScopedModelDelegate<'a, M, PK> {
31        ScopedModelDelegate {
32            delegate: self,
33            ctx,
34        }
35    }
36
37    pub fn find_many(&self) -> FindMany<'a, M, PK> {
38        FindMany {
39            runtime: self.runtime,
40            descriptor: self.descriptor,
41            filters: Vec::new(),
42            order_by: Vec::new(),
43            limit: None,
44            offset: None,
45        }
46    }
47
48    pub fn find_unique(&self, id: PK) -> FindUnique<'a, M, PK> {
49        FindUnique {
50            runtime: self.runtime,
51            descriptor: self.descriptor,
52            id,
53        }
54    }
55
56    pub fn create<I>(&self, input: I) -> CreateRecord<'a, M, PK, I> {
57        CreateRecord {
58            runtime: self.runtime,
59            descriptor: self.descriptor,
60            input,
61        }
62    }
63
64    /// Insert-or-update on primary-key conflict. Available only on models
65    /// whose `@id` field is client-supplied (no `@default(...)`); attempting
66    /// to call this on a model with a server-generated PK is a compile error.
67    pub fn upsert<I>(&self, input: I) -> UpsertRecord<'a, M, PK, I> {
68        UpsertRecord {
69            runtime: self.runtime,
70            descriptor: self.descriptor,
71            input,
72        }
73    }
74
75    pub fn update(&self, id: PK) -> UpdateRecord<'a, M, PK> {
76        UpdateRecord {
77            runtime: self.runtime,
78            descriptor: self.descriptor,
79            id,
80        }
81    }
82
83    pub fn delete(&self, id: PK) -> DeleteRecord<'a, M, PK> {
84        DeleteRecord {
85            runtime: self.runtime,
86            descriptor: self.descriptor,
87            id,
88        }
89    }
90
91    /// Fetch many rows by primary key in a single round-trip; missing rows
92    /// surface as per-item `NotFound` in the envelope rather than aborting.
93    pub fn batch_get(&self, ids: Vec<PK>) -> BatchGet<'a, M, PK> {
94        BatchGet {
95            runtime: self.runtime,
96            descriptor: self.descriptor,
97            ids,
98        }
99    }
100
101    /// Insert many rows in one outer transaction; each input runs under a
102    /// nested SAVEPOINT, so a per-item failure (validation, policy, unique
103    /// conflict) doesn't take down the rest of the batch.
104    pub fn batch_create<I>(&self, inputs: Vec<I>) -> BatchCreate<'a, M, PK, I> {
105        BatchCreate {
106            runtime: self.runtime,
107            descriptor: self.descriptor,
108            inputs,
109        }
110    }
111
112    /// Update many rows in one outer transaction with per-item patches and
113    /// optional `if_match` versions. Per-item failures roll back at the
114    /// savepoint; successful items commit together.
115    pub fn batch_update<I>(
116        &self,
117        items: Vec<BatchUpdateItem<PK, I>>,
118    ) -> BatchUpdate<'a, M, PK, I> {
119        BatchUpdate {
120            runtime: self.runtime,
121            descriptor: self.descriptor,
122            items,
123        }
124    }
125
126    /// Delete many rows by primary key in a single statement; rows that
127    /// don't exist (or that policy hid) surface as per-item `NotFound`.
128    pub fn batch_delete(&self, ids: Vec<PK>) -> BatchDelete<'a, M, PK> {
129        BatchDelete {
130            runtime: self.runtime,
131            descriptor: self.descriptor,
132            ids,
133        }
134    }
135
136    /// Insert-or-update many rows in one outer transaction with per-item
137    /// savepoints. Eligible only for models whose `@id` is client-supplied
138    /// — same compile-time gate as the single-row `.upsert(...)`.
139    pub fn batch_upsert<I>(&self, inputs: Vec<I>) -> BatchUpsert<'a, M, PK, I> {
140        BatchUpsert {
141            runtime: self.runtime,
142            descriptor: self.descriptor,
143            inputs,
144        }
145    }
146
147    pub async fn authorize_detail(&self, id: PK, ctx: &CoolContext) -> Result<(), CoolError>
148    where
149        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
150    {
151        crate::query::authorize_record_action(
152            self.runtime,
153            self.descriptor,
154            id,
155            self.descriptor.detail_allow_policies,
156            self.descriptor.detail_deny_policies,
157            ctx,
158            "detail",
159        )
160        .await
161    }
162
163    pub async fn authorize_update(&self, id: PK, ctx: &CoolContext) -> Result<(), CoolError>
164    where
165        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
166    {
167        crate::query::authorize_record_action(
168            self.runtime,
169            self.descriptor,
170            id,
171            self.descriptor.update_allow_policies,
172            self.descriptor.update_deny_policies,
173            ctx,
174            "update",
175        )
176        .await
177    }
178
179    pub async fn authorize_delete(&self, id: PK, ctx: &CoolContext) -> Result<(), CoolError>
180    where
181        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
182    {
183        crate::query::authorize_record_action(
184            self.runtime,
185            self.descriptor,
186            id,
187            self.descriptor.delete_allow_policies,
188            self.descriptor.delete_deny_policies,
189            ctx,
190            "delete",
191        )
192        .await
193    }
194}
195
196#[derive(Debug, Clone)]
197pub struct ScopedModelDelegate<'a, M: 'static, PK: 'static> {
198    delegate: ModelDelegate<'a, M, PK>,
199    ctx: CoolContext,
200}
201
202impl<'a, M: 'static, PK: 'static> ScopedModelDelegate<'a, M, PK> {
203    pub fn descriptor(&self) -> &'static ModelDescriptor<M, PK> {
204        self.delegate.descriptor()
205    }
206
207    pub fn context(&self) -> &CoolContext {
208        &self.ctx
209    }
210
211    pub fn find_many(&self) -> ScopedFindMany<'a, M, PK> {
212        ScopedFindMany {
213            request: self.delegate.find_many(),
214            ctx: self.ctx.clone(),
215        }
216    }
217
218    pub fn find_unique(&self, id: PK) -> ScopedFindUnique<'a, M, PK> {
219        ScopedFindUnique {
220            request: self.delegate.find_unique(id),
221            ctx: self.ctx.clone(),
222        }
223    }
224
225    pub fn create<I>(&self, input: I) -> ScopedCreateRecord<'a, M, PK, I> {
226        ScopedCreateRecord {
227            request: self.delegate.create(input),
228            ctx: self.ctx.clone(),
229        }
230    }
231
232    pub fn upsert<I>(&self, input: I) -> ScopedUpsertRecord<'a, M, PK, I> {
233        ScopedUpsertRecord {
234            request: self.delegate.upsert(input),
235            ctx: self.ctx.clone(),
236        }
237    }
238
239    pub fn update(&self, id: PK) -> ScopedUpdateRecord<'a, M, PK> {
240        ScopedUpdateRecord {
241            request: self.delegate.update(id),
242            ctx: self.ctx.clone(),
243        }
244    }
245
246    pub fn delete(&self, id: PK) -> ScopedDeleteRecord<'a, M, PK> {
247        ScopedDeleteRecord {
248            request: self.delegate.delete(id),
249            ctx: self.ctx.clone(),
250        }
251    }
252
253    pub fn batch_get(&self, ids: Vec<PK>) -> ScopedBatchGet<'a, M, PK> {
254        ScopedBatchGet {
255            request: self.delegate.batch_get(ids),
256            ctx: self.ctx.clone(),
257        }
258    }
259
260    pub fn batch_create<I>(&self, inputs: Vec<I>) -> ScopedBatchCreate<'a, M, PK, I> {
261        ScopedBatchCreate {
262            request: self.delegate.batch_create(inputs),
263            ctx: self.ctx.clone(),
264        }
265    }
266
267    pub fn batch_update<I>(
268        &self,
269        items: Vec<BatchUpdateItem<PK, I>>,
270    ) -> ScopedBatchUpdate<'a, M, PK, I> {
271        ScopedBatchUpdate {
272            request: self.delegate.batch_update(items),
273            ctx: self.ctx.clone(),
274        }
275    }
276
277    pub fn batch_delete(&self, ids: Vec<PK>) -> ScopedBatchDelete<'a, M, PK> {
278        ScopedBatchDelete {
279            request: self.delegate.batch_delete(ids),
280            ctx: self.ctx.clone(),
281        }
282    }
283
284    pub fn batch_upsert<I>(&self, inputs: Vec<I>) -> ScopedBatchUpsert<'a, M, PK, I> {
285        ScopedBatchUpsert {
286            request: self.delegate.batch_upsert(inputs),
287            ctx: self.ctx.clone(),
288        }
289    }
290}
291
292#[derive(Debug, Clone)]
293pub struct ScopedFindMany<'a, M: 'static, PK: 'static> {
294    request: FindMany<'a, M, PK>,
295    ctx: CoolContext,
296}
297
298impl<'a, M: 'static, PK: 'static> ScopedFindMany<'a, M, PK> {
299    pub fn where_(mut self, filter: Filter) -> Self {
300        self.request = self.request.where_(filter);
301        self
302    }
303
304    pub fn where_expr(mut self, filter: FilterExpr) -> Self {
305        self.request = self.request.where_expr(filter);
306        self
307    }
308
309    pub fn where_any(mut self, filters: impl IntoIterator<Item = FilterExpr>) -> Self {
310        self.request = self.request.where_any(filters);
311        self
312    }
313
314    pub fn order_by(mut self, clause: OrderClause) -> Self {
315        self.request = self.request.order_by(clause);
316        self
317    }
318
319    pub fn limit(mut self, limit: i64) -> Self {
320        self.request = self.request.limit(limit);
321        self
322    }
323
324    pub fn offset(mut self, offset: i64) -> Self {
325        self.request = self.request.offset(offset);
326        self
327    }
328
329    pub fn preview_sql(&self) -> String {
330        self.request.preview_sql()
331    }
332
333    pub fn preview_scoped_sql(&self) -> String {
334        self.request.preview_scoped_sql(&self.ctx)
335    }
336
337    pub async fn run(self) -> Result<Vec<M>, CoolError>
338    where
339        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
340    {
341        self.request.run(&self.ctx).await
342    }
343}
344
345#[derive(Debug, Clone)]
346pub struct ScopedFindUnique<'a, M: 'static, PK: 'static> {
347    request: FindUnique<'a, M, PK>,
348    ctx: CoolContext,
349}
350
351impl<'a, M: 'static, PK: 'static> ScopedFindUnique<'a, M, PK> {
352    pub fn preview_sql(&self) -> String {
353        self.request.preview_sql()
354    }
355
356    pub fn preview_scoped_sql(&self) -> String {
357        self.request.preview_scoped_sql(&self.ctx)
358    }
359
360    pub async fn run(self) -> Result<Option<M>, CoolError>
361    where
362        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
363        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
364    {
365        self.request.run(&self.ctx).await
366    }
367}
368
369#[derive(Debug, Clone)]
370pub struct ScopedCreateRecord<'a, M: 'static, PK: 'static, I> {
371    request: CreateRecord<'a, M, PK, I>,
372    ctx: CoolContext,
373}
374
375impl<'a, M: 'static, PK: 'static, I> ScopedCreateRecord<'a, M, PK, I>
376where
377    I: CreateModelInput<M>,
378{
379    pub fn preview_sql(&self) -> String {
380        self.request.preview_sql()
381    }
382
383    pub async fn run(self) -> Result<M, CoolError>
384    where
385        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
386    {
387        self.request.run(&self.ctx).await
388    }
389}
390
391#[derive(Debug, Clone)]
392pub struct ScopedUpsertRecord<'a, M: 'static, PK: 'static, I> {
393    request: UpsertRecord<'a, M, PK, I>,
394    ctx: CoolContext,
395}
396
397impl<'a, M: 'static, PK: 'static, I> ScopedUpsertRecord<'a, M, PK, I>
398where
399    I: UpsertModelInput<M>,
400{
401    pub fn preview_sql(&self) -> String {
402        self.request.preview_sql()
403    }
404
405    pub async fn run(self) -> Result<M, CoolError>
406    where
407        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
408        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
409    {
410        self.request.run(&self.ctx).await
411    }
412}
413
414#[derive(Debug, Clone)]
415pub struct ScopedUpdateRecord<'a, M: 'static, PK: 'static> {
416    request: UpdateRecord<'a, M, PK>,
417    ctx: CoolContext,
418}
419
420impl<'a, M: 'static, PK: 'static> ScopedUpdateRecord<'a, M, PK> {
421    pub fn set<I>(self, input: I) -> ScopedUpdateRecordSet<'a, M, PK, I> {
422        ScopedUpdateRecordSet {
423            request: self.request.set(input),
424            ctx: self.ctx,
425        }
426    }
427}
428
429#[derive(Debug, Clone)]
430pub struct ScopedUpdateRecordSet<'a, M: 'static, PK: 'static, I> {
431    request: UpdateRecordSet<'a, M, PK, I>,
432    ctx: CoolContext,
433}
434
435impl<'a, M: 'static, PK: 'static, I> ScopedUpdateRecordSet<'a, M, PK, I>
436where
437    I: UpdateModelInput<M>,
438{
439    pub fn preview_sql(&self) -> String {
440        self.request.preview_sql()
441    }
442
443    pub async fn run(self) -> Result<M, CoolError>
444    where
445        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
446        PK: Send + Clone + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
447    {
448        self.request.run(&self.ctx).await
449    }
450
451    /// Attach an expected version for optimistic locking. See
452    /// [`UpdateRecordSet::if_match`].
453    pub fn if_match(mut self, expected: i64) -> Self {
454        self.request = self.request.if_match(expected);
455        self
456    }
457}
458
459#[derive(Debug, Clone)]
460pub struct ScopedDeleteRecord<'a, M: 'static, PK: 'static> {
461    request: DeleteRecord<'a, M, PK>,
462    ctx: CoolContext,
463}
464
465impl<'a, M: 'static, PK: 'static> ScopedDeleteRecord<'a, M, PK> {
466    pub fn preview_sql(&self) -> String {
467        self.request.preview_sql()
468    }
469
470    pub async fn run(self) -> Result<M, CoolError>
471    where
472        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
473        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
474    {
475        self.request.run(&self.ctx).await
476    }
477}
478
479// ───── Scoped batch wrappers ────────────────────────────────────────────────
480//
481// Thin lifetime-and-context shims around the unscoped batch builders. The
482// shape mirrors the existing `ScopedCreateRecord` / `ScopedUpdateRecord`
483// pairs: capture the request-bound `CoolContext` once at `.bind(ctx)` time,
484// thread it into `.run()` automatically.
485
486use std::hash::Hash;
487
488use cratestack_core::BatchResponse;
489
490#[derive(Debug, Clone)]
491pub struct ScopedBatchGet<'a, M: 'static, PK: 'static> {
492    request: BatchGet<'a, M, PK>,
493    ctx: CoolContext,
494}
495
496impl<'a, M: 'static, PK: 'static> ScopedBatchGet<'a, M, PK> {
497    pub async fn run(self) -> Result<BatchResponse<M>, CoolError>
498    where
499        for<'r> M:
500            Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + crate::ModelPrimaryKey<PK>,
501        PK: Clone
502            + Eq
503            + Hash
504            + Send
505            + sqlx::Type<sqlx::Postgres>
506            + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
507    {
508        self.request.run(&self.ctx).await
509    }
510}
511
512#[derive(Debug, Clone)]
513pub struct ScopedBatchCreate<'a, M: 'static, PK: 'static, I> {
514    request: BatchCreate<'a, M, PK, I>,
515    ctx: CoolContext,
516}
517
518impl<'a, M: 'static, PK: 'static, I> ScopedBatchCreate<'a, M, PK, I>
519where
520    I: CreateModelInput<M> + Send,
521{
522    pub async fn run(self) -> Result<BatchResponse<M>, CoolError>
523    where
524        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
525    {
526        self.request.run(&self.ctx).await
527    }
528}
529
530#[derive(Debug, Clone)]
531pub struct ScopedBatchUpdate<'a, M: 'static, PK: 'static, I> {
532    request: BatchUpdate<'a, M, PK, I>,
533    ctx: CoolContext,
534}
535
536impl<'a, M: 'static, PK: 'static, I> ScopedBatchUpdate<'a, M, PK, I>
537where
538    I: UpdateModelInput<M> + Send,
539{
540    pub async fn run(self) -> Result<BatchResponse<M>, CoolError>
541    where
542        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
543        PK: Clone
544            + Eq
545            + Hash
546            + Send
547            + sqlx::Type<sqlx::Postgres>
548            + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
549    {
550        self.request.run(&self.ctx).await
551    }
552}
553
554#[derive(Debug, Clone)]
555pub struct ScopedBatchDelete<'a, M: 'static, PK: 'static> {
556    request: BatchDelete<'a, M, PK>,
557    ctx: CoolContext,
558}
559
560impl<'a, M: 'static, PK: 'static> ScopedBatchDelete<'a, M, PK> {
561    pub async fn run(self) -> Result<BatchResponse<M>, CoolError>
562    where
563        for<'r> M: Send
564            + Unpin
565            + sqlx::FromRow<'r, sqlx::postgres::PgRow>
566            + crate::ModelPrimaryKey<PK>
567            + serde::Serialize,
568        PK: Clone
569            + Eq
570            + Hash
571            + Send
572            + sqlx::Type<sqlx::Postgres>
573            + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
574    {
575        self.request.run(&self.ctx).await
576    }
577}
578
579#[derive(Debug, Clone)]
580pub struct ScopedBatchUpsert<'a, M: 'static, PK: 'static, I> {
581    request: BatchUpsert<'a, M, PK, I>,
582    ctx: CoolContext,
583}
584
585impl<'a, M: 'static, PK: 'static, I> ScopedBatchUpsert<'a, M, PK, I>
586where
587    I: UpsertModelInput<M>,
588{
589    pub async fn run(self) -> Result<BatchResponse<M>, CoolError>
590    where
591        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
592        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
593    {
594        self.request.run(&self.ctx).await
595    }
596}