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 {}