Skip to main content

dynoxide/actions/
mod.rs

1pub mod batch_execute_statement;
2pub mod batch_get_item;
3pub mod batch_write_item;
4pub mod create_table;
5pub mod delete_item;
6pub mod delete_table;
7pub mod describe_stream;
8pub mod describe_table;
9pub mod describe_time_to_live;
10pub mod execute_statement;
11pub mod execute_transaction;
12pub mod get_item;
13pub mod get_records;
14pub mod get_shard_iterator;
15pub(crate) mod gsi;
16pub(crate) mod helpers;
17pub mod import_items;
18pub mod list_streams;
19pub mod list_tables;
20pub mod list_tags_of_resource;
21pub(crate) mod lsi;
22pub mod put_item;
23pub mod query;
24pub mod scan;
25pub mod tag_resource;
26pub mod transact_get_items;
27pub mod transact_write_items;
28pub mod untag_resource;
29pub mod update_item;
30pub mod update_table;
31pub mod update_time_to_live;
32
33use crate::types::{
34    AttributeDefinition, GlobalSecondaryIndex, KeySchemaElement, LocalSecondaryIndex, Projection,
35};
36use serde::{Deserialize, Serialize};
37
38/// Full table description returned by DescribeTable and CreateTable.
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
40pub struct TableDescription {
41    #[serde(rename = "TableName")]
42    pub table_name: String,
43
44    #[serde(rename = "TableId", skip_serializing_if = "Option::is_none")]
45    pub table_id: Option<String>,
46
47    #[serde(rename = "TableArn")]
48    pub table_arn: String,
49
50    #[serde(rename = "TableStatus")]
51    pub table_status: String,
52
53    #[serde(rename = "KeySchema")]
54    pub key_schema: Vec<KeySchemaElement>,
55
56    #[serde(rename = "AttributeDefinitions")]
57    pub attribute_definitions: Vec<AttributeDefinition>,
58
59    #[serde(rename = "CreationDateTime", skip_serializing_if = "Option::is_none")]
60    pub creation_date_time: Option<f64>,
61
62    #[serde(rename = "ItemCount", skip_serializing_if = "Option::is_none")]
63    pub item_count: Option<i64>,
64
65    #[serde(rename = "TableSizeBytes", skip_serializing_if = "Option::is_none")]
66    pub table_size_bytes: Option<i64>,
67
68    #[serde(
69        rename = "ProvisionedThroughput",
70        skip_serializing_if = "Option::is_none"
71    )]
72    pub provisioned_throughput: Option<TableProvisionedThroughputDescription>,
73
74    #[serde(rename = "BillingModeSummary", skip_serializing_if = "Option::is_none")]
75    pub billing_mode_summary: Option<BillingModeSummary>,
76
77    #[serde(
78        rename = "TableThroughputModeSummary",
79        skip_serializing_if = "Option::is_none"
80    )]
81    pub table_throughput_mode_summary: Option<TableThroughputModeSummary>,
82
83    #[serde(
84        rename = "GlobalSecondaryIndexes",
85        skip_serializing_if = "Option::is_none"
86    )]
87    pub global_secondary_indexes: Option<Vec<GlobalSecondaryIndexDescription>>,
88
89    #[serde(
90        rename = "LocalSecondaryIndexes",
91        skip_serializing_if = "Option::is_none"
92    )]
93    pub local_secondary_indexes: Option<Vec<LocalSecondaryIndexDescription>>,
94
95    #[serde(
96        rename = "StreamSpecification",
97        skip_serializing_if = "Option::is_none"
98    )]
99    pub stream_specification: Option<StreamSpecificationDescription>,
100
101    #[serde(rename = "LatestStreamArn", skip_serializing_if = "Option::is_none")]
102    pub latest_stream_arn: Option<String>,
103
104    #[serde(rename = "LatestStreamLabel", skip_serializing_if = "Option::is_none")]
105    pub latest_stream_label: Option<String>,
106
107    #[serde(rename = "SSEDescription", skip_serializing_if = "Option::is_none")]
108    pub sse_description: Option<SseDescription>,
109
110    #[serde(rename = "TableClassSummary", skip_serializing_if = "Option::is_none")]
111    pub table_class_summary: Option<TableClassSummary>,
112
113    #[serde(
114        rename = "DeletionProtectionEnabled",
115        skip_serializing_if = "Option::is_none"
116    )]
117    pub deletion_protection_enabled: Option<bool>,
118}
119
120/// SSE description returned in TableDescription.
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
122pub struct SseDescription {
123    #[serde(rename = "Status")]
124    pub status: String,
125    #[serde(rename = "SSEType", skip_serializing_if = "Option::is_none")]
126    pub sse_type: Option<String>,
127    #[serde(rename = "KMSMasterKeyArn", skip_serializing_if = "Option::is_none")]
128    pub kms_master_key_arn: Option<String>,
129}
130
131/// Stream specification description returned in TableDescription.
132#[derive(Debug, Clone, Default, Serialize, Deserialize)]
133pub struct StreamSpecificationDescription {
134    #[serde(rename = "StreamEnabled")]
135    pub stream_enabled: bool,
136    #[serde(rename = "StreamViewType", skip_serializing_if = "Option::is_none")]
137    pub stream_view_type: Option<String>,
138}
139
140/// Table class summary returned in TableDescription.
141#[derive(Debug, Clone, Default, Serialize, Deserialize)]
142pub struct TableClassSummary {
143    #[serde(rename = "TableClass")]
144    pub table_class: String,
145}
146
147/// Provisioned throughput description (with additional metadata fields).
148#[derive(Debug, Clone, Default, Serialize, Deserialize)]
149pub struct TableProvisionedThroughputDescription {
150    #[serde(rename = "ReadCapacityUnits")]
151    pub read_capacity_units: u64,
152    #[serde(rename = "WriteCapacityUnits")]
153    pub write_capacity_units: u64,
154    #[serde(rename = "NumberOfDecreasesToday")]
155    pub number_of_decreases_today: u64,
156    #[serde(
157        rename = "LastIncreaseDateTime",
158        skip_serializing_if = "Option::is_none"
159    )]
160    pub last_increase_date_time: Option<f64>,
161    #[serde(
162        rename = "LastDecreaseDateTime",
163        skip_serializing_if = "Option::is_none"
164    )]
165    pub last_decrease_date_time: Option<f64>,
166}
167
168/// Billing mode summary.
169#[derive(Debug, Clone, Default, Serialize, Deserialize)]
170pub struct BillingModeSummary {
171    #[serde(rename = "BillingMode")]
172    pub billing_mode: String,
173    #[serde(
174        rename = "LastUpdateToPayPerRequestDateTime",
175        skip_serializing_if = "Option::is_none"
176    )]
177    pub last_update_to_pay_per_request_date_time: Option<f64>,
178}
179
180/// Table throughput mode summary.
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
182pub struct TableThroughputModeSummary {
183    #[serde(rename = "TableThroughputMode")]
184    pub table_throughput_mode: String,
185    #[serde(
186        rename = "LastUpdateToPayPerRequestDateTime",
187        skip_serializing_if = "Option::is_none"
188    )]
189    pub last_update_to_pay_per_request_date_time: Option<f64>,
190}
191
192/// GSI description (returned in TableDescription).
193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194pub struct GlobalSecondaryIndexDescription {
195    #[serde(rename = "IndexName")]
196    pub index_name: String,
197    #[serde(rename = "IndexArn")]
198    pub index_arn: String,
199    #[serde(rename = "KeySchema")]
200    pub key_schema: Vec<KeySchemaElement>,
201    #[serde(rename = "Projection")]
202    pub projection: Projection,
203    #[serde(rename = "IndexStatus")]
204    pub index_status: String,
205    #[serde(
206        rename = "ProvisionedThroughput",
207        skip_serializing_if = "Option::is_none"
208    )]
209    pub provisioned_throughput: Option<TableProvisionedThroughputDescription>,
210    #[serde(rename = "ItemCount", skip_serializing_if = "Option::is_none")]
211    pub item_count: Option<i64>,
212    #[serde(rename = "IndexSizeBytes", skip_serializing_if = "Option::is_none")]
213    pub index_size_bytes: Option<i64>,
214}
215
216/// LSI description (returned in TableDescription).
217#[derive(Debug, Clone, Default, Serialize, Deserialize)]
218pub struct LocalSecondaryIndexDescription {
219    #[serde(rename = "IndexName")]
220    pub index_name: String,
221    #[serde(rename = "IndexArn")]
222    pub index_arn: String,
223    #[serde(rename = "KeySchema")]
224    pub key_schema: Vec<KeySchemaElement>,
225    #[serde(rename = "Projection")]
226    pub projection: Projection,
227    #[serde(rename = "ItemCount", skip_serializing_if = "Option::is_none")]
228    pub item_count: Option<i64>,
229    #[serde(rename = "IndexSizeBytes", skip_serializing_if = "Option::is_none")]
230    pub index_size_bytes: Option<i64>,
231}
232
233/// Generate a UUID v4 for TableId.
234fn generate_table_id() -> String {
235    uuid::Uuid::new_v4().to_string()
236}
237
238/// Helper: Build a TableDescription from stored metadata.
239pub(crate) fn build_table_description(
240    meta: &crate::storage::TableMetadata,
241    item_count: Option<i64>,
242    table_size_bytes: Option<i64>,
243) -> TableDescription {
244    use crate::streams;
245
246    let key_schema: Vec<KeySchemaElement> =
247        serde_json::from_str(&meta.key_schema).unwrap_or_default();
248    let attribute_definitions: Vec<AttributeDefinition> =
249        serde_json::from_str(&meta.attribute_definitions).unwrap_or_default();
250
251    let gsi_definitions: Option<Vec<GlobalSecondaryIndex>> = meta
252        .gsi_definitions
253        .as_ref()
254        .and_then(|s| serde_json::from_str(s).ok());
255
256    let table_name = &meta.table_name;
257
258    let global_secondary_indexes = gsi_definitions.map(|gsis| {
259        gsis.into_iter()
260            .map(|gsi| {
261                let idx_arn = streams::index_arn(table_name, &gsi.index_name);
262                GlobalSecondaryIndexDescription {
263                    index_name: gsi.index_name,
264                    index_arn: idx_arn,
265                    key_schema: gsi.key_schema,
266                    projection: gsi.projection,
267                    index_status: "ACTIVE".to_string(),
268                    provisioned_throughput: Some(if let Some(pt) = gsi.provisioned_throughput {
269                        TableProvisionedThroughputDescription {
270                            read_capacity_units: pt.read_capacity_units.unwrap_or(0) as u64,
271                            write_capacity_units: pt.write_capacity_units.unwrap_or(0) as u64,
272                            number_of_decreases_today: 0,
273                            last_increase_date_time: None,
274                            last_decrease_date_time: None,
275                        }
276                    } else {
277                        // PAY_PER_REQUEST or no PT specified
278                        TableProvisionedThroughputDescription {
279                            read_capacity_units: 0,
280                            write_capacity_units: 0,
281                            number_of_decreases_today: 0,
282                            last_increase_date_time: None,
283                            last_decrease_date_time: None,
284                        }
285                    }),
286                    item_count: Some(0),
287                    index_size_bytes: Some(0),
288                }
289            })
290            .collect()
291    });
292
293    let lsi_definitions: Option<Vec<LocalSecondaryIndex>> = meta
294        .lsi_definitions
295        .as_ref()
296        .and_then(|s| serde_json::from_str(s).ok());
297
298    let local_secondary_indexes = lsi_definitions.map(|lsis| {
299        lsis.into_iter()
300            .map(|lsi| {
301                let idx_arn = streams::index_arn(table_name, &lsi.index_name);
302                LocalSecondaryIndexDescription {
303                    index_name: lsi.index_name,
304                    index_arn: idx_arn,
305                    key_schema: lsi.key_schema,
306                    projection: lsi.projection,
307                    item_count: Some(0),
308                    index_size_bytes: Some(0),
309                }
310            })
311            .collect()
312    });
313
314    let billing_mode = meta.billing_mode.clone();
315
316    let provisioned_throughput = if let Some(pt_json) = &meta.provisioned_throughput {
317        // Try parsing extended format (with timestamps) first, fall back to basic
318        serde_json::from_str::<serde_json::Value>(pt_json)
319            .ok()
320            .map(|v| {
321                let rcu = v
322                    .get("ReadCapacityUnits")
323                    .and_then(|v| v.as_i64())
324                    .unwrap_or(0) as u64;
325                let wcu = v
326                    .get("WriteCapacityUnits")
327                    .and_then(|v| v.as_i64())
328                    .unwrap_or(0) as u64;
329                let last_inc = v.get("LastIncreaseDateTime").and_then(|v| v.as_f64());
330                let last_dec = v.get("LastDecreaseDateTime").and_then(|v| v.as_f64());
331                let num_dec = v
332                    .get("NumberOfDecreasesToday")
333                    .and_then(|v| v.as_u64())
334                    .unwrap_or(0);
335                TableProvisionedThroughputDescription {
336                    read_capacity_units: rcu,
337                    write_capacity_units: wcu,
338                    number_of_decreases_today: num_dec,
339                    last_increase_date_time: last_inc,
340                    last_decrease_date_time: last_dec,
341                }
342            })
343    } else if billing_mode.as_deref() == Some("PAY_PER_REQUEST") {
344        // PAY_PER_REQUEST tables have zero provisioned throughput
345        Some(TableProvisionedThroughputDescription {
346            read_capacity_units: 0,
347            write_capacity_units: 0,
348            number_of_decreases_today: 0,
349            last_increase_date_time: None,
350            last_decrease_date_time: None,
351        })
352    } else {
353        None
354    };
355
356    let stream_specification = if meta.stream_enabled {
357        Some(StreamSpecificationDescription {
358            stream_enabled: true,
359            stream_view_type: meta.stream_view_type.clone(),
360        })
361    } else {
362        None
363    };
364
365    let latest_stream_arn = if meta.stream_enabled {
366        meta.stream_label
367            .as_ref()
368            .map(|label| streams::stream_arn(table_name, label))
369    } else {
370        None
371    };
372
373    let latest_stream_label = if meta.stream_enabled {
374        meta.stream_label.clone()
375    } else {
376        None
377    };
378
379    // Build SSE description from stored specification
380    let sse_description = meta.sse_specification.as_ref().and_then(|json| {
381        serde_json::from_str::<crate::types::SseSpecification>(json)
382            .ok()
383            .map(|spec| SseDescription {
384                status: if spec.enabled.unwrap_or(false) {
385                    "ENABLED".to_string()
386                } else {
387                    "DISABLED".to_string()
388                },
389                sse_type: spec.sse_type,
390                kms_master_key_arn: spec.kms_master_key_id,
391            })
392    });
393
394    let table_class_summary = meta.table_class.as_ref().map(|tc| TableClassSummary {
395        table_class: tc.clone(),
396    });
397
398    let deletion_protection_enabled = Some(meta.deletion_protection_enabled);
399
400    TableDescription {
401        table_name: meta.table_name.clone(),
402        table_id: Some(generate_table_id()),
403        table_arn: streams::table_arn(table_name),
404        table_status: meta.table_status.clone(),
405        key_schema,
406        attribute_definitions,
407        creation_date_time: Some(meta.created_at as f64),
408        item_count,
409        table_size_bytes,
410        provisioned_throughput,
411        billing_mode_summary: match billing_mode.as_deref() {
412            Some("PAY_PER_REQUEST") => Some(BillingModeSummary {
413                billing_mode: "PAY_PER_REQUEST".to_string(),
414                last_update_to_pay_per_request_date_time: None,
415            }),
416            _ => None,
417        },
418        table_throughput_mode_summary: match billing_mode.as_deref() {
419            Some("PAY_PER_REQUEST") => Some(TableThroughputModeSummary {
420                table_throughput_mode: "PAY_PER_REQUEST".to_string(),
421                last_update_to_pay_per_request_date_time: None,
422            }),
423            _ => None,
424        },
425        global_secondary_indexes,
426        local_secondary_indexes,
427        stream_specification,
428        latest_stream_arn,
429        latest_stream_label,
430        sse_description,
431        table_class_summary,
432        deletion_protection_enabled,
433    }
434}