// gsi_handlers.odin — GSI-related HTTP handler helpers // // This file lives in the main package alongside main.odin. // It provides: // 1. parse_global_secondary_indexes — parse GSI definitions from CreateTable request // 2. parse_index_name — extract IndexName from Query/Scan requests // 3. Projection type helper for response building package main import "core:encoding/json" import "core:strings" import "dynamodb" // ============================================================================ // Parse GlobalSecondaryIndexes from CreateTable request body // // DynamoDB CreateTable request format for GSIs: // { // "GlobalSecondaryIndexes": [ // { // "IndexName": "email-index", // "KeySchema": [ // { "AttributeName": "email", "KeyType": "HASH" }, // { "AttributeName": "timestamp", "KeyType": "RANGE" } // ], // "Projection": { // "ProjectionType": "ALL" | "KEYS_ONLY" | "INCLUDE", // "NonKeyAttributes": ["attr1", "attr2"] // only for INCLUDE // } // } // ] // } // // Returns nil if no GSI definitions are present (valid — GSIs are optional). // ============================================================================ parse_global_secondary_indexes :: proc( root: json.Object, attr_defs: []dynamodb.Attribute_Definition, ) -> Maybe([]dynamodb.Global_Secondary_Index) { gsi_val, found := root["GlobalSecondaryIndexes"] if !found { return nil } gsi_arr, ok := gsi_val.(json.Array) if !ok || len(gsi_arr) == 0 { return nil } gsis := make([]dynamodb.Global_Secondary_Index, len(gsi_arr)) for elem, i in gsi_arr { elem_obj, elem_ok := elem.(json.Object) if !elem_ok { cleanup_parsed_gsis(gsis[:i]) delete(gsis) return nil } gsi, gsi_ok := parse_single_gsi(elem_obj, attr_defs) if !gsi_ok { cleanup_parsed_gsis(gsis[:i]) delete(gsis) return nil } gsis[i] = gsi } return gsis } @(private = "file") parse_single_gsi :: proc( obj: json.Object, attr_defs: []dynamodb.Attribute_Definition, ) -> (dynamodb.Global_Secondary_Index, bool) { gsi: dynamodb.Global_Secondary_Index // IndexName (required) idx_val, idx_found := obj["IndexName"] if !idx_found { return {}, false } idx_str, idx_ok := idx_val.(json.String) if !idx_ok { return {}, false } gsi.index_name = strings.clone(string(idx_str)) // KeySchema (required) ks_val, ks_found := obj["KeySchema"] if !ks_found { delete(gsi.index_name) return {}, false } ks_arr, ks_ok := ks_val.(json.Array) if !ks_ok || len(ks_arr) == 0 || len(ks_arr) > 2 { delete(gsi.index_name) return {}, false } key_schema := make([]dynamodb.Key_Schema_Element, len(ks_arr)) hash_count := 0 for ks_elem, j in ks_arr { ks_obj, kobj_ok := ks_elem.(json.Object) if !kobj_ok { for k in 0.. 0 { nka := make([]string, len(nka_arr)) for attr_val, k in nka_arr { if attr_str, attr_ok := attr_val.(json.String); attr_ok { nka[k] = strings.clone(string(attr_str)) } } gsi.projection.non_key_attributes = nka } } } } return gsi, true } @(private = "file") cleanup_parsed_gsis :: proc(gsis: []dynamodb.Global_Secondary_Index) { for gsi in gsis { delete(gsi.index_name) for ks in gsi.key_schema { delete(ks.attribute_name) } delete(gsi.key_schema) if nka, has_nka := gsi.projection.non_key_attributes.?; has_nka { for attr in nka { delete(attr) } delete(nka) } } } // ============================================================================ // Parse IndexName from Query/Scan request // ============================================================================ parse_index_name :: proc(request_body: []byte) -> Maybe(string) { data, parse_err := json.parse(request_body, allocator = context.temp_allocator) if parse_err != nil { return nil } defer json.destroy_value(data) root, root_ok := data.(json.Object) if !root_ok { return nil } idx_val, found := root["IndexName"] if !found { return nil } idx_str, ok := idx_val.(json.String) if !ok { return nil } return string(idx_str) } // ============================================================================ // Projection type to string for DescribeTable response // ============================================================================ projection_type_to_string :: proc(pt: dynamodb.Projection_Type) -> string { switch pt { case .ALL: return "ALL" case .KEYS_ONLY: return "KEYS_ONLY" case .INCLUDE: return "INCLUDE" } return "ALL" }