Skip to main content

dynamodb_facade/operations/
mod.rs

1mod batch;
2mod delete;
3mod get;
4mod pagination;
5mod put;
6mod query;
7mod scan;
8mod transactions;
9mod type_state;
10mod update;
11
12pub use batch::*;
13pub use delete::*;
14pub use get::*;
15pub use pagination::*;
16pub use put::*;
17pub use query::*;
18pub use scan::*;
19pub use transactions::*;
20pub use type_state::*;
21pub use update::*;
22
23use serde::{Serialize, de::DeserializeOwned};
24use std::marker::PhantomData;
25
26use super::{
27    ApplyCondition, ApplyFilter, ApplyKeyCondition, ApplyProjection, ApplyUpdate,
28    AttributeDefinition, Condition, DynamoDBItem, HasAttribute, HasConstAttribute,
29    HasIndexKeyAttributes, IndexDefinition, Item, Key, KeyCondition, KeyConditionState, KeySchema,
30    PartitionKeyDefinition, Projection, Result, TableDefinition, Update,
31};
32
33// ---------------------------------------------------------------------------
34// DynamoDBItemOp trait — typed operation entry points
35// ---------------------------------------------------------------------------
36
37/// Primary entry point for typed single-item and collection CRUD operations.
38///
39/// This trait is **blanket-implemented** for every type that implements
40/// [`DynamoDBItem<TD>`]. You never implement it manually — implement
41/// `DynamoDBItem` (via the `dynamodb_item!` macro) and all methods here become
42/// available automatically.
43///
44/// Every method returns a builder with compile-time safety guarantees that
45/// mirror DynamoDB API constraints. For example, calling `.condition()` twice
46/// is a compile error (DynamoDB accepts one condition expression per request),
47/// and `.project()` automatically switches to raw output since projected
48/// results may be incomplete for deserialization.
49///
50/// # Operation overview
51///
52/// | Method | DynamoDB operation | Default return |
53/// |---|---|---|
54/// | [`get`][DynamoDBItemOp::get] | `GetItem` | `Option<T>` |
55/// | [`put`][DynamoDBItemOp::put] | `PutItem` | `()` |
56/// | [`delete`][DynamoDBItemOp::delete] | `DeleteItem` | `()` |
57/// | [`delete_by_id`][DynamoDBItemOp::delete_by_id] | `DeleteItem` | `Option<T>` (old) |
58/// | [`update`][DynamoDBItemOp::update] | `UpdateItem` | `()` |
59/// | [`update_by_id`][DynamoDBItemOp::update_by_id] | `UpdateItem` | `T` (new) |
60/// | [`scan`][DynamoDBItemOp::scan] | `Scan` | `Vec<T>` |
61/// | [`scan_index`][DynamoDBItemOp::scan_index] | `Scan` (LSI/GSI) | `Vec<T>` |
62/// | [`query`][DynamoDBItemOp::query] | `Query` | `Vec<T>` |
63/// | [`query_all`][DynamoDBItemOp::query_all] | `Query` (const PK) | `Vec<T>` |
64/// | [`query_index`][DynamoDBItemOp::query_index] | `Query` (LSI/GSI) | `Vec<T>` |
65/// | [`query_all_index`][DynamoDBItemOp::query_all_index] | `Query` (LSI/GSI, const PK) | `Vec<T>` |
66///
67/// # Examples
68///
69/// ```no_run
70/// # use dynamodb_facade::test_fixtures::*;
71/// use dynamodb_facade::{DynamoDBItemOp, KeyId, Update, Condition};
72///
73/// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
74/// # let client = cclient.clone();
75/// // Get a user by ID
76/// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1")).await?;
77///
78/// # let client = cclient.clone();
79/// // Get with consistent read
80/// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
81///     .consistent_read()
82///     .await?;
83///
84/// # let client = cclient.clone();
85/// // Put a new user (unconditional)
86/// sample_user().put(client).await?;
87///
88/// # let client = cclient.clone();
89/// // Put a new user (create-only)
90/// sample_user().put(client).not_exists().await?;
91///
92/// # let client = cclient.clone();
93/// // Put with a custom condition
94/// sample_user()
95///     .put(client)
96///     .condition(User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000))
97///     .await?;
98///
99/// # let client = cclient.clone();
100/// // Put and return the old item
101/// let old /* : Option<User> */ = sample_user().put(client).return_old().await?;
102///
103/// # let client = cclient.clone();
104/// // Delete an enrollment (unconditional)
105/// sample_enrollment().delete(client).await?;
106///
107/// # let client = cclient.clone();
108/// // Delete only if the item exists
109/// sample_enrollment().delete(client).exists().await?;
110///
111/// # let client = cclient.clone();
112/// // Delete with a custom condition
113/// sample_enrollment()
114///     .delete(client)
115///     .condition(Enrollment::exists() & Condition::not_exists("completed_at"))
116///     .await?;
117///
118/// # let client = cclient.clone();
119/// // Delete by ID and return the old item
120/// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
121///     client,
122///     KeyId::pk("user-1").sk("course-42"),
123/// )
124/// .exists()
125/// .await?;
126///
127/// # let client = cclient.clone();
128/// // Update a user's role (fire-and-forget)
129/// sample_user()
130///     .update(client, Update::set("role", "instructor"))
131///     .exists()
132///     .await?;
133///
134/// # let client = cclient.clone();
135/// // Update by ID and return the updated item (default for update_by_id)
136/// let updated /* : User */ = User::update_by_id(
137///     client,
138///     KeyId::pk("user-1"),
139///     Update::set("name", "Bob"),
140/// )
141/// .exists()
142/// .await?;
143///
144/// # let client = cclient.clone();
145/// // Update with a custom condition
146/// let updated /* : User */ = User::update_by_id(
147///     client,
148///     KeyId::pk("user-1"),
149///     Update::set("role", "instructor"),
150/// )
151/// .condition(Condition::eq("role", "student"))
152/// .await?;
153///
154/// # let client = cclient.clone();
155/// // Update without returning the item
156/// User::update_by_id(
157///     client,
158///     KeyId::pk("user-1"),
159///     Update::set("name", "Bob"),
160/// )
161/// .exists()
162/// .return_none()
163/// .await?;
164///
165/// # let client = cclient.clone();
166/// // Query all enrollments for a user
167/// let enrollments /* : Vec<Enrollment> */ =
168///     Enrollment::query(client, Enrollment::key_condition("user-1"))
169///         .all()
170///         .await?;
171///
172/// # let client = cclient.clone();
173/// // Scan all users with a filter
174/// let instructors /* : Vec<User> */ = User::scan(client)
175///     .filter(Condition::eq("role", "instructor"))
176///     .all()
177///     .await?;
178/// # Ok(())
179/// # }
180/// ```
181pub trait DynamoDBItemOp<TD: TableDefinition>: DynamoDBItem<TD> {
182    /// Returns a [`GetItemRequest`] builder in `Typed` output mode for
183    /// fetching a single item by key.
184    ///
185    /// The returned builder can be `.await`ed directly (returns
186    /// `Option<T>`), or further configured with
187    /// [`.raw()`][GetItemRequest::raw],
188    /// [`.project()`][GetItemRequest::project], or
189    /// [`.consistent_read()`][GetItemRequest::consistent_read].
190    ///
191    /// # Examples
192    ///
193    /// ```no_run
194    /// # use dynamodb_facade::test_fixtures::*;
195    /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
196    ///
197    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
198    /// # let client = cclient.clone();
199    /// // Simple get by ID
200    /// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1")).await?;
201    ///
202    /// # let client = cclient.clone();
203    /// // Consistent read
204    /// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
205    ///     .consistent_read()
206    ///     .await?;
207    /// # Ok(())
208    /// # }
209    /// ```
210    fn get(
211        client: aws_sdk_dynamodb::Client,
212        key_id: Self::KeyId<'_>,
213    ) -> GetItemRequest<TD, Self, Typed> {
214        GetItemRequest::_new(client, Self::get_key_from_id(key_id))
215    }
216
217    /// Returns a [`PutItemRequest`] builder in `Typed` output mode with
218    /// `ReturnNothing` and no condition.
219    ///
220    /// The returned builder can be `.await`ed directly, or further configured
221    /// with [`.not_exists()`][PutItemRequest::not_exists],
222    /// [`.condition()`][PutItemRequest::condition],
223    /// [`.return_old()`][PutItemRequest::return_old], or
224    /// [`.raw()`][PutItemRequest::raw].
225    ///
226    /// # Panics
227    ///
228    /// Panics if serializing `self` via [`DynamoDBItem::to_item`] fails. See
229    /// [`DynamoDBItem::to_item`] for the conditions under which this can
230    /// happen — it is the caller's responsibility to provide a compatible
231    /// [`Serialize`] implementation.
232    ///
233    /// # Examples
234    ///
235    /// ```no_run
236    /// # use dynamodb_facade::test_fixtures::*;
237    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
238    ///
239    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
240    /// let user = sample_user();
241    ///
242    /// # let client = cclient.clone();
243    /// // Unconditional put (overwrites any existing item)
244    /// user.put(client).await?;
245    ///
246    /// # let client = cclient.clone();
247    /// // Create-only: fails if item already exists
248    /// user.put(client).not_exists().await?;
249    ///
250    /// # let client = cclient.clone();
251    /// // Custom condition: create-only OR expired TTL
252    /// user.put(client)
253    ///     .condition(User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000))
254    ///     .await?;
255    ///
256    /// # let client = cclient.clone();
257    /// // Put and return the old item
258    /// let old /* : Option<User> */ = user.put(client).return_old().await?;
259    /// # Ok(())
260    /// # }
261    /// ```
262    fn put(&self, client: aws_sdk_dynamodb::Client) -> PutItemRequest<TD, Self, Typed>
263    where
264        Self: Serialize,
265    {
266        PutItemRequest::_new(client, self.to_item())
267    }
268
269    /// Returns a [`DeleteItemRequest`] builder in `Typed` output mode with
270    /// `ReturnNothing` and no condition.
271    ///
272    /// The returned builder can be `.await`ed directly, or further configured
273    /// with [`.exists()`][DeleteItemRequest::exists],
274    /// [`.condition()`][DeleteItemRequest::condition],
275    /// [`.return_old()`][DeleteItemRequest::return_old], or
276    /// [`.raw()`][DeleteItemRequest::raw].
277    ///
278    /// # Examples
279    ///
280    /// ```no_run
281    /// # use dynamodb_facade::test_fixtures::*;
282    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
283    ///
284    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
285    /// let enrollment = sample_enrollment();
286    ///
287    /// # let client = cclient.clone();
288    /// // Unconditional delete
289    /// enrollment.delete(client).await?;
290    ///
291    /// # let client = cclient.clone();
292    /// // Delete only if the item exists
293    /// enrollment.delete(client).exists().await?;
294    ///
295    /// # let client = cclient.clone();
296    /// // Delete with a custom condition
297    /// enrollment
298    ///     .delete(client)
299    ///     .condition(Enrollment::exists() & Condition::not_exists("completed_at"))
300    ///     .await?;
301    ///
302    /// # let client = cclient.clone();
303    /// // Delete and return the old item
304    /// let old /* : Option<Enrollment> */ = enrollment.delete(client).return_old().await?;
305    /// # Ok(())
306    /// # }
307    /// ```
308    fn delete(&self, client: aws_sdk_dynamodb::Client) -> DeleteItemRequest<TD, Self, Typed> {
309        DeleteItemRequest::_new(client, self.get_key())
310    }
311
312    /// Returns a [`DeleteItemRequest`] builder in `Typed` output mode with
313    /// `Return<Old>` and no condition.
314    ///
315    /// Unlike [`delete`][DynamoDBItemOp::delete], this method accepts a
316    /// `KeyId` instead of a loaded instance, and defaults to
317    /// `Return<Old>` — the deleted item is returned as `Option<T>`.
318    ///
319    /// The returned builder can be further configured with
320    /// [`.exists()`][DeleteItemRequest::exists],
321    /// [`.condition()`][DeleteItemRequest::condition],
322    /// [`.return_none()`][DeleteItemRequest::return_none], or
323    /// [`.raw()`][DeleteItemRequest::raw].
324    ///
325    /// # Examples
326    ///
327    /// ```no_run
328    /// # use dynamodb_facade::test_fixtures::*;
329    /// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyId};
330    ///
331    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
332    /// # let client = cclient.clone();
333    /// // Simple delete by ID (returns the old item by default)
334    /// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
335    ///     client,
336    ///     KeyId::pk("user-1").sk("course-42"),
337    /// )
338    /// .await?;
339    ///
340    /// # let client = cclient.clone();
341    /// // Delete only if the item exists
342    /// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
343    ///     client,
344    ///     KeyId::pk("user-1").sk("course-42"),
345    /// )
346    /// .exists()
347    /// .await?;
348    ///
349    /// # let client = cclient.clone();
350    /// // Delete with a custom condition
351    /// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
352    ///     client,
353    ///     KeyId::pk("user-1").sk("course-42"),
354    /// )
355    /// .condition(Enrollment::exists() & Condition::not_exists("completed_at"))
356    /// .await?;
357    ///
358    /// # let client = cclient.clone();
359    /// // Delete without returning the old item
360    /// Enrollment::delete_by_id(
361    ///     client,
362    ///     KeyId::pk("user-1").sk("course-42"),
363    /// )
364    /// .return_none()
365    /// .await?;
366    /// # Ok(())
367    /// # }
368    /// ```
369    fn delete_by_id(
370        client: aws_sdk_dynamodb::Client,
371        key_id: Self::KeyId<'_>,
372    ) -> DeleteItemRequest<TD, Self, Typed, Return<Old>> {
373        DeleteItemRequest::_new(client, Self::get_key_from_id(key_id)).return_old()
374    }
375
376    /// Returns an [`UpdateItemRequest`] builder in `Typed` output mode with
377    /// `ReturnNothing` and no condition.
378    ///
379    /// The returned builder can be `.await`ed directly, or further configured
380    /// with [`.exists()`][UpdateItemRequest::exists],
381    /// [`.condition()`][UpdateItemRequest::condition],
382    /// [`.return_new()`][UpdateItemRequest::return_new],
383    /// [`.return_old()`][UpdateItemRequest::return_old], or
384    /// [`.raw()`][UpdateItemRequest::raw].
385    ///
386    /// # Examples
387    ///
388    /// ```no_run
389    /// # use dynamodb_facade::test_fixtures::*;
390    /// use dynamodb_facade::{DynamoDBItemOp, Condition, Update};
391    ///
392    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
393    /// let user = sample_user();
394    ///
395    /// # let client = cclient.clone();
396    /// // Simple update (fire-and-forget)
397    /// user.update(client, Update::set("role", "instructor")).await?;
398    ///
399    /// # let client = cclient.clone();
400    /// // Update guarded by existence
401    /// user.update(client, Update::set("role", "instructor"))
402    ///     .exists()
403    ///     .await?;
404    ///
405    /// # let client = cclient.clone();
406    /// // Update with a custom condition
407    /// user.update(client, Update::set("role", "instructor"))
408    ///     .condition(Condition::eq("role", "student"))
409    ///     .await?;
410    ///
411    /// # let client = cclient.clone();
412    /// // Update if exist and return the new item
413    /// let updated /* : User */ = user
414    ///     .update(client, Update::set("name", "Alice B."))
415    ///     .exists()
416    ///     .return_new()
417    ///     .await?;
418    /// # Ok(())
419    /// # }
420    /// ```
421    fn update(
422        &self,
423        client: aws_sdk_dynamodb::Client,
424        update: Update<'_>,
425    ) -> UpdateItemRequest<TD, Self, Typed> {
426        UpdateItemRequest::_new(client, self.get_key(), update)
427    }
428
429    /// Returns an [`UpdateItemRequest`] builder in `Typed` output mode with
430    /// `Return<New>` and no condition.
431    ///
432    /// Unlike [`update`][DynamoDBItemOp::update], this method accepts a
433    /// `KeyId` instead of a loaded instance, and defaults to `Return<New>` —
434    /// the updated item is returned as `T`.
435    ///
436    /// The returned builder can be further configured with
437    /// [`.exists()`][UpdateItemRequest::exists],
438    /// [`.condition()`][UpdateItemRequest::condition],
439    /// [`.return_none()`][UpdateItemRequest::return_none],
440    /// [`.return_old()`][UpdateItemRequest::return_old], or
441    /// [`.raw()`][UpdateItemRequest::raw].
442    ///
443    /// # Examples
444    ///
445    /// ```no_run
446    /// # use dynamodb_facade::test_fixtures::*;
447    /// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyId, Update};
448    ///
449    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
450    /// # let client = cclient.clone();
451    /// // Simple update by ID (returns the updated item by default)
452    /// let updated /* : User */ = User::update_by_id(
453    ///     client,
454    ///     KeyId::pk("user-1"),
455    ///     Update::set("role", "instructor"),
456    /// )
457    /// .await?;
458    ///
459    /// # let client = cclient.clone();
460    /// // Update guarded by existence
461    /// let updated /* : User */ = User::update_by_id(
462    ///     client,
463    ///     KeyId::pk("user-1"),
464    ///     Update::set("role", "instructor"),
465    /// )
466    /// .exists()
467    /// .await?;
468    ///
469    /// # let client = cclient.clone();
470    /// // Update with a custom condition
471    /// let updated /* : User */ = User::update_by_id(
472    ///     client,
473    ///     KeyId::pk("user-1"),
474    ///     Update::set("role", "instructor"),
475    /// )
476    /// .condition(Condition::eq("role", "student"))
477    /// .await?;
478    ///
479    /// # let client = cclient.clone();
480    /// // Update without returning the item
481    /// User::update_by_id(
482    ///     client,
483    ///     KeyId::pk("user-1"),
484    ///     Update::set("name", "Bob"),
485    /// )
486    /// .exists()
487    /// .return_none()
488    /// .await?;
489    /// # Ok(())
490    /// # }
491    /// ```
492    fn update_by_id(
493        client: aws_sdk_dynamodb::Client,
494        key_id: Self::KeyId<'_>,
495        update: Update<'_>,
496    ) -> UpdateItemRequest<TD, Self, Typed, Return<New>> {
497        UpdateItemRequest::_new(client, Self::get_key_from_id(key_id), update)
498    }
499
500    /// Returns a [`ScanRequest`] builder in `Typed` output mode for scanning
501    /// the entire table.
502    ///
503    /// The returned builder can be executed with
504    /// [`.all()`][ScanRequest::all] or [`.stream()`][ScanRequest::stream],
505    /// and further configured with [`.filter()`][ScanRequest::filter],
506    /// [`.project()`][ScanRequest::project], [`.limit()`][ScanRequest::limit],
507    /// or [`.raw()`][ScanRequest::raw].
508    ///
509    /// Prefer [`query`][DynamoDBItemOp::query] when possible — scans read every
510    /// item in the table and are significantly more expensive.
511    ///
512    /// Also, note that this method will fail in most cases because a DynamoDB table
513    /// rarely contains only items of the same type. Use
514    /// [`scan_index`][DynamoDBItemOp::scan_index] to scan an index that may contain
515    /// only a specific type of item, or [`.raw()`][ScanRequest::raw] to prevent item
516    /// deserialization.
517    ///
518    /// # Examples
519    ///
520    /// ```no_run
521    /// # use dynamodb_facade::test_fixtures::*;
522    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
523    ///
524    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
525    /// # let client = cclient.clone();
526    /// // Scan the table and attempts projecting all items as users
527    /// let all_users /* : Vec<User> */ = User::scan(client).all().await?;
528    ///
529    /// # let client = cclient.clone();
530    /// // Scan with a filter (prefer using query on an appropriate index)
531    /// let instructors /* : Vec<User> */ = User::scan(client)
532    ///     .filter(Condition::eq("role", "instructor"))
533    ///     .all()
534    ///     .await?;
535    /// # Ok(())
536    /// # }
537    /// ```
538    fn scan(client: aws_sdk_dynamodb::Client) -> ScanRequest<TD, Self, Typed> {
539        ScanRequest::_new(client)
540    }
541
542    /// Returns a [`ScanRequest`] builder in `Typed` output mode for scanning
543    /// a secondary index (GSI or LSI).
544    ///
545    /// `I` must be an [`IndexDefinition`] for `TD`, and `Self` must implement
546    /// [`HasAttribute<A>`] for evey keys of the IndexDefinition to confirm the
547    /// type participates in that index.
548    ///
549    /// Also, note that this method will fail is the index does not contains only items
550    /// of the expected type. Use [`.raw()`][ScanRequest::raw] to prevent item
551    /// deserialization.
552    ///
553    /// # Examples
554    ///
555    /// ```no_run
556    /// # use dynamodb_facade::test_fixtures::*;
557    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
558    ///
559    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
560    /// # let client = cclient.clone();
561    /// // Scan an index and attempts projecting all items as enrollments
562    /// let all_enrollments /* : Vec<Enrollment> */ =
563    ///     Enrollment::scan_index::<TypeIndex>(client)
564    ///         .all()
565    ///         .await?;
566    ///
567    /// # let client = cclient.clone();
568    /// // Scan an index with a filter (prefer using query on an appropriate index)
569    /// let recent /* : Vec<Enrollment> */ =
570    ///     Enrollment::scan_index::<TypeIndex>(client)
571    ///         .filter(Condition::gt("enrolled_at", 1_700_000_000))
572    ///         .all()
573    ///         .await?;
574    /// # Ok(())
575    /// # }
576    /// ```
577    fn scan_index<I: IndexDefinition<TD>>(
578        client: aws_sdk_dynamodb::Client,
579    ) -> ScanRequest<TD, Self, Typed>
580    where
581        Self: HasIndexKeyAttributes<TD, I>,
582    {
583        ScanRequest::_new_index::<I>(client)
584    }
585
586    /// Returns a [`QueryRequest`] builder in `Typed` output mode for querying
587    /// the table with the given key condition.
588    ///
589    /// The returned builder can be executed with
590    /// [`.all()`][QueryRequest::all] or [`.stream()`][QueryRequest::stream],
591    /// and further configured with [`.filter()`][QueryRequest::filter],
592    /// [`.project()`][QueryRequest::project], [`.limit()`][QueryRequest::limit],
593    /// [`.reverse()`][QueryRequest::reverse], or
594    /// [`.raw()`][QueryRequest::raw].
595    ///
596    /// # Examples
597    ///
598    /// ```no_run
599    /// # use dynamodb_facade::test_fixtures::*;
600    /// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyCondition};
601    ///
602    /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
603    /// # let client = cclient.clone();
604    /// // Query all enrollments for a specific user
605    /// let enrollments /* : Vec<Enrollment> */ = Enrollment::query(
606    ///     client,
607    ///     Enrollment::key_condition("user-1").sk_begins_with("ENROLL#"),
608    /// )
609    /// .all()
610    /// .await?;
611    ///
612    /// # let client = cclient.clone();
613    /// // Query with a filter
614    /// let advanced /* : Vec<Enrollment> */ =
615    ///     Enrollment::query(client, Enrollment::key_condition("user-1"))
616    ///         .filter(Condition::gt("progress", 0.5))
617    ///         .all()
618    ///         .await?;
619    /// # Ok(())
620    /// # }
621    /// ```
622    fn query(
623        client: aws_sdk_dynamodb::Client,
624        key_condition: KeyCondition<'_, TD::KeySchema, impl KeyConditionState>,
625    ) -> QueryRequest<TD, Self, Typed> {
626        QueryRequest::_new(client, key_condition)
627    }
628
629    /// Returns a [`QueryRequest`] builder in `Typed` output mode, using the
630    /// type's constant partition key value as the key condition.
631    ///
632    /// Available only when `Self` has a compile-time constant value for the
633    /// table's partition key (i.e. implements
634    /// `HasConstAttribute<TD::KeySchema::PartitionKey>`).
635    ///
636    /// # Examples
637    ///
638    /// ```no_run
639    /// # use dynamodb_facade::test_fixtures::*;
640    /// use dynamodb_facade::DynamoDBItemOp;
641    ///
642    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
643    /// // Query all items stored under the constant PlatformConfig PK
644    /// let configs /* : Vec<PlatformConfig> */ = PlatformConfig::query_all(client)
645    ///     .all()
646    ///     .await?;
647    /// # Ok(())
648    /// # }
649    /// ```
650    fn query_all(client: aws_sdk_dynamodb::Client) -> QueryRequest<TD, Self, Typed>
651    where
652        Self: HasConstAttribute<<TD::KeySchema as KeySchema>::PartitionKey>,
653    {
654        Self::query(client, KeyCondition::pk(Self::VALUE))
655    }
656
657    /// Returns a [`QueryRequest`] builder in `Typed` output mode for querying
658    /// a secondary index (GSI or LSI) with the given key condition.
659    ///
660    /// `I` must be an [`IndexDefinition`] for `TD`, and `Self` must implement
661    /// [`HasAttribute<A>`] for evey keys of the IndexDefinition. The key
662    /// condition is typed to the index's key schema, preventing mismatched
663    /// attribute usage at compile time.
664    ///
665    /// # Examples
666    ///
667    /// ```no_run
668    /// # use dynamodb_facade::test_fixtures::*;
669    /// use dynamodb_facade::{DynamoDBItemOp, KeyCondition};
670    ///
671    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
672    /// // Query a secondary index
673    /// let users /* : Vec<User> */ = User::query_index::<EmailIndex>(
674    ///     client,
675    ///     KeyCondition::pk("alice@example.com".to_owned()),
676    /// )
677    /// .all()
678    /// .await?;
679    /// # Ok(())
680    /// # }
681    /// ```
682    fn query_index<I: IndexDefinition<TD>>(
683        client: aws_sdk_dynamodb::Client,
684        key_condition: KeyCondition<'_, I::KeySchema, impl KeyConditionState>,
685    ) -> QueryRequest<TD, Self, Typed>
686    where
687        Self: HasIndexKeyAttributes<TD, I>,
688    {
689        QueryRequest::_new_index::<I>(client, key_condition)
690    }
691
692    /// Returns a [`QueryRequest`] builder in `Typed` output mode for querying
693    /// a secondary index (GSI or LSI) using the type's constant PK value for
694    /// that index.
695    ///
696    /// Available only when `Self` has a compile-time constant value for the
697    /// index's partition key (i.e. implements
698    /// `HasConstAttribute<I::KeySchema::PartitionKey>`).
699    ///
700    /// # Examples
701    ///
702    /// ```no_run
703    /// # use dynamodb_facade::test_fixtures::*;
704    /// use dynamodb_facade::DynamoDBItemOp;
705    ///
706    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
707    /// // Query all users via the TypeIndex (constant ItemType = "USER")
708    /// let all_users /* : Vec<User> */ = User::query_all_index::<TypeIndex>(client)
709    ///     .all()
710    ///     .await?;
711    /// # Ok(())
712    /// # }
713    /// ```
714    fn query_all_index<I: IndexDefinition<TD>>(
715        client: aws_sdk_dynamodb::Client,
716    ) -> QueryRequest<TD, Self, Typed>
717    where
718        Self: HasIndexKeyAttributes<TD, I>
719            + HasConstAttribute<<I::KeySchema as KeySchema>::PartitionKey>,
720    {
721        Self::query_index::<I>(client, KeyCondition::pk(Self::VALUE))
722    }
723
724    // -- Condition helpers ----------------------------------------------------
725
726    /// Returns a [`Condition`] that checks whether an item exists.
727    ///
728    /// Generates `attribute_exists(<PK>)` using the table's partition key
729    /// attribute name. Useful as a guard on put, delete, and update operations,
730    /// or as a component in compound conditions.
731    ///
732    /// See also `exists` shorthand methods on the individual request builders.
733    ///
734    /// # Examples
735    ///
736    /// ```
737    /// # use dynamodb_facade::test_fixtures::*;
738    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
739    ///
740    /// // Use as a standalone condition
741    /// let cond = User::exists();
742    ///
743    /// // Combine with another condition using `&`
744    /// let cond = User::exists() & Condition::eq("role", "admin");
745    /// ```
746    fn exists() -> Condition<'static> {
747        Condition::exists(<TD::KeySchema as KeySchema>::PartitionKey::NAME)
748    }
749
750    /// Returns a [`Condition`] that checks whether an item does not exist.
751    ///
752    /// Generates `attribute_not_exists(<PK>)` using the table's partition key
753    /// attribute name. Commonly used with [`put`][DynamoDBItemOp::put] to
754    /// implement create-only semantics.
755    ///
756    /// See also `not_exists` shorthand methods on the individual request builders.
757    ///
758    /// # Examples
759    ///
760    /// ```
761    /// # use dynamodb_facade::test_fixtures::*;
762    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
763    ///
764    /// // Use as a standalone condition
765    /// let cond = User::not_exists();
766    ///
767    /// // Combine: create-only OR expired TTL
768    /// let cond = User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000);
769    /// ```
770    fn not_exists() -> Condition<'static> {
771        Condition::not_exists(<TD::KeySchema as KeySchema>::PartitionKey::NAME)
772    }
773
774    // -- Key Condition helpers ------------------------------------------------
775
776    /// Builds a [`KeyCondition`] for the table's partition key from a typed ID.
777    ///
778    /// Uses the type's [`HasAttribute`] implementation to convert `pk_id` into
779    /// the DynamoDB attribute value for the partition key. The resulting
780    /// condition can be extended with sort-key constraints before being passed
781    /// to [`query`][DynamoDBItemOp::query] methods.
782    ///
783    /// # Examples
784    ///
785    /// ```
786    /// # use dynamodb_facade::test_fixtures::*;
787    /// use dynamodb_facade::DynamoDBItemOp;
788    ///
789    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
790    /// // All enrollments for a user
791    /// let kc = Enrollment::key_condition("user-1").sk_begins_with("ENROLL#");
792    /// let enrollments /* : Vec<Enrollment> */ = Enrollment::query(client, kc)
793    ///     .all()
794    ///     .await?;
795    /// # Ok(())
796    /// # }
797    /// ```
798    fn key_condition(
799        pk_id: <Self as HasAttribute<PartitionKeyDefinition<TD>>>::Id<'_>,
800    ) -> KeyCondition<'static, TD::KeySchema> {
801        KeyCondition::pk(<Self as HasAttribute<PartitionKeyDefinition<TD>>>::attribute_value(pk_id))
802    }
803
804    /// Builds a [`KeyCondition`] for a secondary index's partition key from a typed ID.
805    ///
806    /// Uses the type's [`HasAttribute`] implementation for the index's
807    /// partition key attribute to convert `pk_id` into the appropriate
808    /// DynamoDB value. The resulting condition is typed to the index's key
809    /// schema, so sort-key methods are only available when the index has a
810    /// sort key.
811    ///
812    /// # Examples
813    ///
814    /// ```
815    /// # use dynamodb_facade::test_fixtures::*;
816    /// use dynamodb_facade::DynamoDBItemOp;
817    ///
818    /// // Build a key condition for the EmailIndex
819    /// let kc = User::index_key_condition::<EmailIndex>("alice@example.com");
820    /// let _ = kc;
821    /// ```
822    fn index_key_condition<I: IndexDefinition<TD>>(
823        pk_id: <Self as HasAttribute<<I::KeySchema as KeySchema>::PartitionKey>>::Id<'_>,
824    ) -> KeyCondition<'static, I::KeySchema>
825    where
826        Self: HasAttribute<<I::KeySchema as KeySchema>::PartitionKey>,
827    {
828        KeyCondition::pk(<Self as HasAttribute<
829            <I::KeySchema as KeySchema>::PartitionKey,
830        >>::attribute_value(pk_id))
831    }
832}
833
834impl<TD: TableDefinition, DBI: DynamoDBItem<TD>> DynamoDBItemOp<TD> for DBI {}