Skip to main content

rusticity_term/dynamodb/
items.rs

1//! Explicit, bounded Items requests. Local table pages never advance an AWS cursor.
2use super::actions::is_items_tab;
3use crate::app::{App, Service};
4use crate::keymap::Mode;
5use crate::ui::dynamodb::{self, ItemOperation, ItemProjection, State};
6use rusticity_core::dynamodb::{DdbItemCursor, DdbItemsPage};
7
8#[derive(Clone, Debug, PartialEq)]
9pub struct ItemRequestContext {
10    pub profile: String,
11    pub region: String,
12    pub account_id: String,
13    pub role_arn: String,
14    pub table: String,
15    pub index: Option<String>,
16    pub keys: Vec<String>,
17    pub partition_type: String,
18    pub operation: ItemOperation,
19    pub projection: ItemProjection,
20    /// Scan text is a local filter, not part of the AWS request context.
21    pub query: Option<String>,
22}
23
24pub fn context(app: &App) -> Option<ItemRequestContext> {
25    let table = dynamodb::current_table(app)?;
26    let state = &app.dynamodb_state;
27    Some(ItemRequestContext {
28        profile: app.profile.clone(),
29        region: app.config.region.clone(),
30        account_id: app.config.account_id.clone(),
31        role_arn: app.config.role_arn.clone(),
32        table: table.name.clone(),
33        index: dynamodb::selected_item_index(table, state.item_index),
34        keys: dynamodb::selected_key_attributes(table, state.item_index),
35        partition_type: dynamodb::key_attribute_type(&dynamodb::selected_partition_key_label(
36            table,
37            state.item_index,
38        )),
39        operation: state.item_operation,
40        projection: state.item_projection,
41        query: (state.item_operation == ItemOperation::Query).then(|| state.item_filter.clone()),
42    })
43}
44
45pub fn can_load_more(app: &App) -> bool {
46    let state = &app.dynamodb_state;
47    app.current_service == Service::DynamoDbTables
48        && is_items_tab(app)
49        && !state.items_loading
50        && state.items_cursor.is_some()
51        && state.items_context.is_some()
52        && state.items_context == context(app)
53}
54
55/// A finite total is known only after a successful request for the current
56/// context exhausts its cursor. A disabled Load more action is not proof of this.
57pub fn results_complete(app: &App) -> bool {
58    let state = &app.dynamodb_state;
59    !state.items_loading
60        && !state.items_failed
61        && state.items_cursor.is_none()
62        && state.items_context.is_some()
63        && state.items_context == context(app)
64        && (state.item_operation != ItemOperation::Query || !state.item_filter.is_empty())
65}
66
67pub fn needs_load(app: &App) -> bool {
68    let state = &app.dynamodb_state;
69    if !is_items_tab(app) || app.mode == Mode::ErrorModal {
70        return false;
71    }
72    if state.items_loading {
73        return true;
74    }
75    if app.mode == Mode::FilterInput {
76        return false;
77    }
78    // A context change restarts even when the Query key was cleared. The loader
79    // clears the previous results but sends no request for an empty key.
80    if state.items_context.is_some() && state.items_context != context(app) {
81        return true;
82    }
83    state.item_operation == ItemOperation::Query
84        && !state.item_filter.is_empty()
85        && state.executed_query != state.item_filter
86}
87
88pub fn load_more(app: &mut App) {
89    if app.mode == Mode::Normal && can_load_more(app) {
90        app.dynamodb_state.items_append = true;
91        app.dynamodb_state.items_loading = true;
92    }
93}
94
95pub fn retry(app: &mut App) {
96    // Keep the failed request's append/replace intent. begin_items_request still
97    // rejects the old cursor if the account, table, or query context has changed.
98    app.dynamodb_state.items_loading = true;
99}
100
101impl State {
102    pub fn clear_items_result(&mut self) {
103        self.items.reset();
104        self.items.items.clear();
105        self.items_cursor = None;
106        self.items_context = None;
107        self.items_append = false;
108        self.items_failed = false;
109        self.items_loading = false;
110        self.executed_query.clear();
111    }
112
113    pub fn begin_items_request(&mut self, context: ItemRequestContext) -> Option<DdbItemCursor> {
114        self.items_append = self.items_append
115            && self.items_context.as_ref() == Some(&context)
116            && self.items_cursor.is_some();
117        if !self.items_append {
118            self.items.reset();
119            self.items.items.clear();
120            self.items_cursor = None;
121        }
122        // Remember attempted queries too: an error must not automatically retry
123        // on every subsequent key press.
124        self.executed_query = context.query.clone().unwrap_or_default();
125        self.items_context = Some(context);
126        self.items_failed = false;
127        self.items_loading = true;
128        self.items_cursor.clone()
129    }
130
131    pub fn finish_items_request(&mut self, page: DdbItemsPage) {
132        self.items.items.extend(page.items);
133        self.items_cursor = page.next_cursor;
134        self.items_loading = false;
135        self.items_append = false;
136        self.items_failed = false;
137    }
138
139    pub fn fail_items_request(&mut self) {
140        self.items_loading = false;
141        self.items_failed = true;
142        // Do not discard rows, cursor, or append intent; Ctrl+R can retry.
143    }
144}
145
146#[cfg(test)]
147mod tests;