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 where
215 Self: DeserializeOwned,
216 {
217 GetItemRequest::_new(client, Self::get_key_from_id(key_id))
218 }
219
220 /// Returns a [`PutItemRequest`] builder in `Typed` output mode with
221 /// `ReturnNothing` and no condition.
222 ///
223 /// The returned builder can be `.await`ed directly, or further configured
224 /// with [`.not_exists()`][PutItemRequest::not_exists],
225 /// [`.condition()`][PutItemRequest::condition],
226 /// [`.return_old()`][PutItemRequest::return_old], or
227 /// [`.raw()`][PutItemRequest::raw].
228 ///
229 /// # Panics
230 ///
231 /// Panics if serializing `self` via [`DynamoDBItem::to_item`] fails. See
232 /// [`DynamoDBItem::to_item`] for the conditions under which this can
233 /// happen — it is the caller's responsibility to provide a compatible
234 /// [`Serialize`] implementation.
235 ///
236 /// # Examples
237 ///
238 /// ```no_run
239 /// # use dynamodb_facade::test_fixtures::*;
240 /// use dynamodb_facade::{DynamoDBItemOp, Condition};
241 ///
242 /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
243 /// let user = sample_user();
244 ///
245 /// # let client = cclient.clone();
246 /// // Unconditional put (overwrites any existing item)
247 /// user.put(client).await?;
248 ///
249 /// # let client = cclient.clone();
250 /// // Create-only: fails if item already exists
251 /// user.put(client).not_exists().await?;
252 ///
253 /// # let client = cclient.clone();
254 /// // Custom condition: create-only OR expired TTL
255 /// user.put(client)
256 /// .condition(User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000))
257 /// .await?;
258 ///
259 /// # let client = cclient.clone();
260 /// // Put and return the old item
261 /// let old /* : Option<User> */ = user.put(client).return_old().await?;
262 /// # Ok(())
263 /// # }
264 /// ```
265 fn put(&self, client: aws_sdk_dynamodb::Client) -> PutItemRequest<TD, Self, Typed>
266 where
267 Self: Serialize,
268 {
269 PutItemRequest::_new(client, self.to_item())
270 }
271
272 /// Returns a [`DeleteItemRequest`] builder in `Typed` output mode with
273 /// `ReturnNothing` and no condition.
274 ///
275 /// The returned builder can be `.await`ed directly, or further configured
276 /// with [`.exists()`][DeleteItemRequest::exists],
277 /// [`.condition()`][DeleteItemRequest::condition],
278 /// [`.return_old()`][DeleteItemRequest::return_old], or
279 /// [`.raw()`][DeleteItemRequest::raw].
280 ///
281 /// # Examples
282 ///
283 /// ```no_run
284 /// # use dynamodb_facade::test_fixtures::*;
285 /// use dynamodb_facade::{DynamoDBItemOp, Condition};
286 ///
287 /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
288 /// let enrollment = sample_enrollment();
289 ///
290 /// # let client = cclient.clone();
291 /// // Unconditional delete
292 /// enrollment.delete(client).await?;
293 ///
294 /// # let client = cclient.clone();
295 /// // Delete only if the item exists
296 /// enrollment.delete(client).exists().await?;
297 ///
298 /// # let client = cclient.clone();
299 /// // Delete with a custom condition
300 /// enrollment
301 /// .delete(client)
302 /// .condition(Enrollment::exists() & Condition::not_exists("completed_at"))
303 /// .await?;
304 ///
305 /// # let client = cclient.clone();
306 /// // Delete and return the old item
307 /// let old /* : Option<Enrollment> */ = enrollment.delete(client).return_old().await?;
308 /// # Ok(())
309 /// # }
310 /// ```
311 fn delete(&self, client: aws_sdk_dynamodb::Client) -> DeleteItemRequest<TD, Self, Typed> {
312 DeleteItemRequest::_new(client, self.get_key())
313 }
314
315 /// Returns a [`DeleteItemRequest`] builder in `Typed` output mode with
316 /// `Return<Old>` and no condition.
317 ///
318 /// Unlike [`delete`][DynamoDBItemOp::delete], this method accepts a
319 /// `KeyId` instead of a loaded instance, and defaults to
320 /// `Return<Old>` — the deleted item is returned as `Option<T>`.
321 ///
322 /// The returned builder can be further configured with
323 /// [`.exists()`][DeleteItemRequest::exists],
324 /// [`.condition()`][DeleteItemRequest::condition],
325 /// [`.return_none()`][DeleteItemRequest::return_none], or
326 /// [`.raw()`][DeleteItemRequest::raw].
327 ///
328 /// # Examples
329 ///
330 /// ```no_run
331 /// # use dynamodb_facade::test_fixtures::*;
332 /// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyId};
333 ///
334 /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
335 /// # let client = cclient.clone();
336 /// // Simple delete by ID (returns the old item by default)
337 /// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
338 /// client,
339 /// KeyId::pk("user-1").sk("course-42"),
340 /// )
341 /// .await?;
342 ///
343 /// # let client = cclient.clone();
344 /// // Delete only if the item exists
345 /// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
346 /// client,
347 /// KeyId::pk("user-1").sk("course-42"),
348 /// )
349 /// .exists()
350 /// .await?;
351 ///
352 /// # let client = cclient.clone();
353 /// // Delete with a custom condition
354 /// let old /* : Option<Enrollment> */ = Enrollment::delete_by_id(
355 /// client,
356 /// KeyId::pk("user-1").sk("course-42"),
357 /// )
358 /// .condition(Enrollment::exists() & Condition::not_exists("completed_at"))
359 /// .await?;
360 ///
361 /// # let client = cclient.clone();
362 /// // Delete without returning the old item
363 /// Enrollment::delete_by_id(
364 /// client,
365 /// KeyId::pk("user-1").sk("course-42"),
366 /// )
367 /// .return_none()
368 /// .await?;
369 /// # Ok(())
370 /// # }
371 /// ```
372 fn delete_by_id(
373 client: aws_sdk_dynamodb::Client,
374 key_id: Self::KeyId<'_>,
375 ) -> DeleteItemRequest<TD, Self, Typed, Return<Old>>
376 where
377 Self: DeserializeOwned,
378 {
379 DeleteItemRequest::_new(client, Self::get_key_from_id(key_id)).return_old()
380 }
381
382 /// Returns an [`UpdateItemRequest`] builder in `Typed` output mode with
383 /// `ReturnNothing` and no condition.
384 ///
385 /// The returned builder can be `.await`ed directly, or further configured
386 /// with [`.exists()`][UpdateItemRequest::exists],
387 /// [`.condition()`][UpdateItemRequest::condition],
388 /// [`.return_new()`][UpdateItemRequest::return_new],
389 /// [`.return_old()`][UpdateItemRequest::return_old], or
390 /// [`.raw()`][UpdateItemRequest::raw].
391 ///
392 /// # Examples
393 ///
394 /// ```no_run
395 /// # use dynamodb_facade::test_fixtures::*;
396 /// use dynamodb_facade::{DynamoDBItemOp, Condition, Update};
397 ///
398 /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
399 /// let user = sample_user();
400 ///
401 /// # let client = cclient.clone();
402 /// // Simple update (fire-and-forget)
403 /// user.update(client, Update::set("role", "instructor")).await?;
404 ///
405 /// # let client = cclient.clone();
406 /// // Update guarded by existence
407 /// user.update(client, Update::set("role", "instructor"))
408 /// .exists()
409 /// .await?;
410 ///
411 /// # let client = cclient.clone();
412 /// // Update with a custom condition
413 /// user.update(client, Update::set("role", "instructor"))
414 /// .condition(Condition::eq("role", "student"))
415 /// .await?;
416 ///
417 /// # let client = cclient.clone();
418 /// // Update if exist and return the new item
419 /// let updated /* : User */ = user
420 /// .update(client, Update::set("name", "Alice B."))
421 /// .exists()
422 /// .return_new()
423 /// .await?;
424 /// # Ok(())
425 /// # }
426 /// ```
427 fn update(
428 &self,
429 client: aws_sdk_dynamodb::Client,
430 update: Update<'_>,
431 ) -> UpdateItemRequest<TD, Self, Typed> {
432 UpdateItemRequest::_new(client, self.get_key(), update)
433 }
434
435 /// Returns an [`UpdateItemRequest`] builder in `Typed` output mode with
436 /// `Return<New>` and no condition.
437 ///
438 /// Unlike [`update`][DynamoDBItemOp::update], this method accepts a
439 /// `KeyId` instead of a loaded instance, and defaults to `Return<New>` —
440 /// the updated item is returned as `T`.
441 ///
442 /// The returned builder can be further configured with
443 /// [`.exists()`][UpdateItemRequest::exists],
444 /// [`.condition()`][UpdateItemRequest::condition],
445 /// [`.return_none()`][UpdateItemRequest::return_none],
446 /// [`.return_old()`][UpdateItemRequest::return_old], or
447 /// [`.raw()`][UpdateItemRequest::raw].
448 ///
449 /// # Examples
450 ///
451 /// ```no_run
452 /// # use dynamodb_facade::test_fixtures::*;
453 /// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyId, Update};
454 ///
455 /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
456 /// # let client = cclient.clone();
457 /// // Simple update by ID (returns the updated item by default)
458 /// let updated /* : User */ = User::update_by_id(
459 /// client,
460 /// KeyId::pk("user-1"),
461 /// Update::set("role", "instructor"),
462 /// )
463 /// .await?;
464 ///
465 /// # let client = cclient.clone();
466 /// // Update guarded by existence
467 /// let updated /* : User */ = User::update_by_id(
468 /// client,
469 /// KeyId::pk("user-1"),
470 /// Update::set("role", "instructor"),
471 /// )
472 /// .exists()
473 /// .await?;
474 ///
475 /// # let client = cclient.clone();
476 /// // Update with a custom condition
477 /// let updated /* : User */ = User::update_by_id(
478 /// client,
479 /// KeyId::pk("user-1"),
480 /// Update::set("role", "instructor"),
481 /// )
482 /// .condition(Condition::eq("role", "student"))
483 /// .await?;
484 ///
485 /// # let client = cclient.clone();
486 /// // Update without returning the item
487 /// User::update_by_id(
488 /// client,
489 /// KeyId::pk("user-1"),
490 /// Update::set("name", "Bob"),
491 /// )
492 /// .exists()
493 /// .return_none()
494 /// .await?;
495 /// # Ok(())
496 /// # }
497 /// ```
498 fn update_by_id(
499 client: aws_sdk_dynamodb::Client,
500 key_id: Self::KeyId<'_>,
501 update: Update<'_>,
502 ) -> UpdateItemRequest<TD, Self, Typed, Return<New>>
503 where
504 Self: DeserializeOwned,
505 {
506 UpdateItemRequest::_new(client, Self::get_key_from_id(key_id), update)
507 }
508
509 /// Returns a [`ScanRequest`] builder in `Typed` output mode for scanning
510 /// the entire table.
511 ///
512 /// The returned builder can be executed with
513 /// [`.all()`][ScanRequest::all] or [`.stream()`][ScanRequest::stream],
514 /// and further configured with [`.filter()`][ScanRequest::filter],
515 /// [`.project()`][ScanRequest::project], [`.limit()`][ScanRequest::limit],
516 /// or [`.raw()`][ScanRequest::raw].
517 ///
518 /// Prefer [`query`][DynamoDBItemOp::query] when possible — scans read every
519 /// item in the table and are significantly more expensive.
520 ///
521 /// Also, note that this method will fail in most cases because a DynamoDB table
522 /// rarely contains only items of the same type. Use
523 /// [`scan_index`][DynamoDBItemOp::scan_index] to scan an index that may contain
524 /// only a specific type of item, or [`.raw()`][ScanRequest::raw] to prevent item
525 /// deserialization.
526 ///
527 /// # Examples
528 ///
529 /// ```no_run
530 /// # use dynamodb_facade::test_fixtures::*;
531 /// use dynamodb_facade::{DynamoDBItemOp, Condition};
532 ///
533 /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
534 /// # let client = cclient.clone();
535 /// // Scan the table and attempts projecting all items as users
536 /// let all_users /* : Vec<User> */ = User::scan(client).all().await?;
537 ///
538 /// # let client = cclient.clone();
539 /// // Scan with a filter (prefer using query on an appropriate index)
540 /// let instructors /* : Vec<User> */ = User::scan(client)
541 /// .filter(Condition::eq("role", "instructor"))
542 /// .all()
543 /// .await?;
544 /// # Ok(())
545 /// # }
546 /// ```
547 fn scan(client: aws_sdk_dynamodb::Client) -> ScanRequest<TD, Self, Typed>
548 where
549 Self: DeserializeOwned,
550 {
551 ScanRequest::_new(client)
552 }
553
554 /// Returns a [`ScanRequest`] builder in `Typed` output mode for scanning
555 /// a secondary index (GSI or LSI).
556 ///
557 /// `I` must be an [`IndexDefinition`] for `TD`, and `Self` must implement
558 /// [`HasAttribute<A>`] for evey keys of the IndexDefinition to confirm the
559 /// type participates in that index.
560 ///
561 /// Also, note that this method will fail is the index does not contains only items
562 /// of the expected type. Use [`.raw()`][ScanRequest::raw] to prevent item
563 /// deserialization.
564 ///
565 /// # Examples
566 ///
567 /// ```no_run
568 /// # use dynamodb_facade::test_fixtures::*;
569 /// use dynamodb_facade::{DynamoDBItemOp, Condition};
570 ///
571 /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
572 /// # let client = cclient.clone();
573 /// // Scan an index and attempts projecting all items as enrollments
574 /// let all_enrollments /* : Vec<Enrollment> */ =
575 /// Enrollment::scan_index::<TypeIndex>(client)
576 /// .all()
577 /// .await?;
578 ///
579 /// # let client = cclient.clone();
580 /// // Scan an index with a filter (prefer using query on an appropriate index)
581 /// let recent /* : Vec<Enrollment> */ =
582 /// Enrollment::scan_index::<TypeIndex>(client)
583 /// .filter(Condition::gt("enrolled_at", 1_700_000_000))
584 /// .all()
585 /// .await?;
586 /// # Ok(())
587 /// # }
588 /// ```
589 fn scan_index<I: IndexDefinition<TD>>(
590 client: aws_sdk_dynamodb::Client,
591 ) -> ScanRequest<TD, Self, Typed>
592 where
593 Self: DeserializeOwned + HasIndexKeyAttributes<TD, I>,
594 {
595 ScanRequest::_new_index::<I>(client)
596 }
597
598 /// Returns a [`QueryRequest`] builder in `Typed` output mode for querying
599 /// the table with the given key condition.
600 ///
601 /// The returned builder can be executed with
602 /// [`.all()`][QueryRequest::all] or [`.stream()`][QueryRequest::stream],
603 /// and further configured with [`.filter()`][QueryRequest::filter],
604 /// [`.project()`][QueryRequest::project], [`.limit()`][QueryRequest::limit],
605 /// [`.reverse()`][QueryRequest::reverse], or
606 /// [`.raw()`][QueryRequest::raw].
607 ///
608 /// # Examples
609 ///
610 /// ```no_run
611 /// # use dynamodb_facade::test_fixtures::*;
612 /// use dynamodb_facade::{DynamoDBItemOp, Condition, KeyCondition};
613 ///
614 /// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
615 /// # let client = cclient.clone();
616 /// // Query all enrollments for a specific user
617 /// let enrollments /* : Vec<Enrollment> */ = Enrollment::query(
618 /// client,
619 /// Enrollment::key_condition("user-1").sk_begins_with("ENROLL#"),
620 /// )
621 /// .all()
622 /// .await?;
623 ///
624 /// # let client = cclient.clone();
625 /// // Query with a filter
626 /// let advanced /* : Vec<Enrollment> */ =
627 /// Enrollment::query(client, Enrollment::key_condition("user-1"))
628 /// .filter(Condition::gt("progress", 0.5))
629 /// .all()
630 /// .await?;
631 /// # Ok(())
632 /// # }
633 /// ```
634 fn query(
635 client: aws_sdk_dynamodb::Client,
636 key_condition: KeyCondition<'_, TD::KeySchema, impl KeyConditionState>,
637 ) -> QueryRequest<TD, Self, Typed>
638 where
639 Self: DeserializeOwned,
640 {
641 QueryRequest::_new(client, key_condition)
642 }
643
644 /// Returns a [`QueryRequest`] builder in `Typed` output mode, using the
645 /// type's constant partition key value as the key condition.
646 ///
647 /// Available only when `Self` has a compile-time constant value for the
648 /// table's partition key (i.e. implements
649 /// `HasConstAttribute<TD::KeySchema::PartitionKey>`).
650 ///
651 /// # Examples
652 ///
653 /// ```no_run
654 /// # use dynamodb_facade::test_fixtures::*;
655 /// use dynamodb_facade::DynamoDBItemOp;
656 ///
657 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
658 /// // Query all items stored under the constant PlatformConfig PK
659 /// let configs /* : Vec<PlatformConfig> */ = PlatformConfig::query_all(client)
660 /// .all()
661 /// .await?;
662 /// # Ok(())
663 /// # }
664 /// ```
665 fn query_all(client: aws_sdk_dynamodb::Client) -> QueryRequest<TD, Self, Typed>
666 where
667 Self: DeserializeOwned + HasConstAttribute<<TD::KeySchema as KeySchema>::PartitionKey>,
668 {
669 Self::query(client, KeyCondition::pk(Self::VALUE))
670 }
671
672 /// Returns a [`QueryRequest`] builder in `Typed` output mode for querying
673 /// a secondary index (GSI or LSI) with the given key condition.
674 ///
675 /// `I` must be an [`IndexDefinition`] for `TD`, and `Self` must implement
676 /// [`HasAttribute<A>`] for evey keys of the IndexDefinition. The key
677 /// condition is typed to the index's key schema, preventing mismatched
678 /// attribute usage at compile time.
679 ///
680 /// # Examples
681 ///
682 /// ```no_run
683 /// # use dynamodb_facade::test_fixtures::*;
684 /// use dynamodb_facade::{DynamoDBItemOp, KeyCondition};
685 ///
686 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
687 /// // Query a secondary index
688 /// let users /* : Vec<User> */ = User::query_index::<EmailIndex>(
689 /// client,
690 /// KeyCondition::pk("alice@example.com".to_owned()),
691 /// )
692 /// .all()
693 /// .await?;
694 /// # Ok(())
695 /// # }
696 /// ```
697 fn query_index<I: IndexDefinition<TD>>(
698 client: aws_sdk_dynamodb::Client,
699 key_condition: KeyCondition<'_, I::KeySchema, impl KeyConditionState>,
700 ) -> QueryRequest<TD, Self, Typed>
701 where
702 Self: DeserializeOwned + HasIndexKeyAttributes<TD, I>,
703 {
704 QueryRequest::_new_index::<I>(client, key_condition)
705 }
706
707 /// Returns a [`QueryRequest`] builder in `Typed` output mode for querying
708 /// a secondary index (GSI or LSI) using the type's constant PK value for
709 /// that index.
710 ///
711 /// Available only when `Self` has a compile-time constant value for the
712 /// index's partition key (i.e. implements
713 /// `HasConstAttribute<I::KeySchema::PartitionKey>`).
714 ///
715 /// # Examples
716 ///
717 /// ```no_run
718 /// # use dynamodb_facade::test_fixtures::*;
719 /// use dynamodb_facade::DynamoDBItemOp;
720 ///
721 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
722 /// // Query all users via the TypeIndex (constant ItemType = "USER")
723 /// let all_users /* : Vec<User> */ = User::query_all_index::<TypeIndex>(client)
724 /// .all()
725 /// .await?;
726 /// # Ok(())
727 /// # }
728 /// ```
729 fn query_all_index<I: IndexDefinition<TD>>(
730 client: aws_sdk_dynamodb::Client,
731 ) -> QueryRequest<TD, Self, Typed>
732 where
733 Self: DeserializeOwned
734 + HasIndexKeyAttributes<TD, I>
735 + HasConstAttribute<<I::KeySchema as KeySchema>::PartitionKey>,
736 {
737 Self::query_index::<I>(client, KeyCondition::pk(Self::VALUE))
738 }
739
740 // -- Condition helpers ----------------------------------------------------
741
742 /// Returns a [`Condition`] that checks whether an item exists.
743 ///
744 /// Generates `attribute_exists(<PK>)` using the table's partition key
745 /// attribute name. Useful as a guard on put, delete, and update operations,
746 /// or as a component in compound conditions.
747 ///
748 /// See also `exists` shorthand methods on the individual request builders.
749 ///
750 /// # Examples
751 ///
752 /// ```
753 /// # use dynamodb_facade::test_fixtures::*;
754 /// use dynamodb_facade::{DynamoDBItemOp, Condition};
755 ///
756 /// // Use as a standalone condition
757 /// let cond = User::exists();
758 ///
759 /// // Combine with another condition using `&`
760 /// let cond = User::exists() & Condition::eq("role", "admin");
761 /// ```
762 fn exists() -> Condition<'static> {
763 Condition::exists(<TD::KeySchema as KeySchema>::PartitionKey::NAME)
764 }
765
766 /// Returns a [`Condition`] that checks whether an item does not exist.
767 ///
768 /// Generates `attribute_not_exists(<PK>)` using the table's partition key
769 /// attribute name. Commonly used with [`put`][DynamoDBItemOp::put] to
770 /// implement create-only semantics.
771 ///
772 /// See also `not_exists` shorthand methods on the individual request builders.
773 ///
774 /// # Examples
775 ///
776 /// ```
777 /// # use dynamodb_facade::test_fixtures::*;
778 /// use dynamodb_facade::{DynamoDBItemOp, Condition};
779 ///
780 /// // Use as a standalone condition
781 /// let cond = User::not_exists();
782 ///
783 /// // Combine: create-only OR expired TTL
784 /// let cond = User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000);
785 /// ```
786 fn not_exists() -> Condition<'static> {
787 Condition::not_exists(<TD::KeySchema as KeySchema>::PartitionKey::NAME)
788 }
789
790 // -- Key Condition helpers ------------------------------------------------
791
792 /// Builds a [`KeyCondition`] for the table's partition key from a typed ID.
793 ///
794 /// Uses the type's [`HasAttribute`] implementation to convert `pk_id` into
795 /// the DynamoDB attribute value for the partition key. The resulting
796 /// condition can be extended with sort-key constraints before being passed
797 /// to [`query`][DynamoDBItemOp::query] methods.
798 ///
799 /// # Examples
800 ///
801 /// ```
802 /// # use dynamodb_facade::test_fixtures::*;
803 /// use dynamodb_facade::DynamoDBItemOp;
804 ///
805 /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
806 /// // All enrollments for a user
807 /// let kc = Enrollment::key_condition("user-1").sk_begins_with("ENROLL#");
808 /// let enrollments /* : Vec<Enrollment> */ = Enrollment::query(client, kc)
809 /// .all()
810 /// .await?;
811 /// # Ok(())
812 /// # }
813 /// ```
814 fn key_condition(
815 pk_id: <Self as HasAttribute<PartitionKeyDefinition<TD>>>::Id<'_>,
816 ) -> KeyCondition<'static, TD::KeySchema> {
817 KeyCondition::pk(<Self as HasAttribute<PartitionKeyDefinition<TD>>>::attribute_value(pk_id))
818 }
819
820 /// Builds a [`KeyCondition`] for a secondary index's partition key from a typed ID.
821 ///
822 /// Uses the type's [`HasAttribute`] implementation for the index's
823 /// partition key attribute to convert `pk_id` into the appropriate
824 /// DynamoDB value. The resulting condition is typed to the index's key
825 /// schema, so sort-key methods are only available when the index has a
826 /// sort key.
827 ///
828 /// # Examples
829 ///
830 /// ```
831 /// # use dynamodb_facade::test_fixtures::*;
832 /// use dynamodb_facade::DynamoDBItemOp;
833 ///
834 /// // Build a key condition for the EmailIndex
835 /// let kc = User::index_key_condition::<EmailIndex>("alice@example.com");
836 /// let _ = kc;
837 /// ```
838 fn index_key_condition<I: IndexDefinition<TD>>(
839 pk_id: <Self as HasAttribute<<I::KeySchema as KeySchema>::PartitionKey>>::Id<'_>,
840 ) -> KeyCondition<'static, I::KeySchema>
841 where
842 Self: HasAttribute<<I::KeySchema as KeySchema>::PartitionKey>,
843 {
844 KeyCondition::pk(<Self as HasAttribute<
845 <I::KeySchema as KeySchema>::PartitionKey,
846 >>::attribute_value(pk_id))
847 }
848}
849
850impl<TD: TableDefinition, DBI: DynamoDBItem<TD>> DynamoDBItemOp<TD> for DBI {}