277 lines
7.0 KiB
Odin
277 lines
7.0 KiB
Odin
// 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..<j { delete(key_schema[k].attribute_name) }
|
|
delete(key_schema)
|
|
delete(gsi.index_name)
|
|
return {}, false
|
|
}
|
|
|
|
an_val, an_found := ks_obj["AttributeName"]
|
|
if !an_found {
|
|
for k in 0..<j { delete(key_schema[k].attribute_name) }
|
|
delete(key_schema)
|
|
delete(gsi.index_name)
|
|
return {}, false
|
|
}
|
|
an_str, an_ok := an_val.(json.String)
|
|
if !an_ok {
|
|
for k in 0..<j { delete(key_schema[k].attribute_name) }
|
|
delete(key_schema)
|
|
delete(gsi.index_name)
|
|
return {}, false
|
|
}
|
|
|
|
kt_val, kt_found := ks_obj["KeyType"]
|
|
if !kt_found {
|
|
for k in 0..<j { delete(key_schema[k].attribute_name) }
|
|
delete(key_schema)
|
|
delete(gsi.index_name)
|
|
return {}, false
|
|
}
|
|
kt_str, kt_ok := kt_val.(json.String)
|
|
if !kt_ok {
|
|
for k in 0..<j { delete(key_schema[k].attribute_name) }
|
|
delete(key_schema)
|
|
delete(gsi.index_name)
|
|
return {}, false
|
|
}
|
|
|
|
kt, kt_parse_ok := dynamodb.key_type_from_string(string(kt_str))
|
|
if !kt_parse_ok {
|
|
for k in 0..<j { delete(key_schema[k].attribute_name) }
|
|
delete(key_schema)
|
|
delete(gsi.index_name)
|
|
return {}, false
|
|
}
|
|
|
|
if kt == .HASH {
|
|
hash_count += 1
|
|
}
|
|
|
|
// Validate that the GSI key attribute is in AttributeDefinitions
|
|
attr_defined := false
|
|
for ad in attr_defs {
|
|
if ad.attribute_name == string(an_str) {
|
|
attr_defined = true
|
|
break
|
|
}
|
|
}
|
|
if !attr_defined {
|
|
for k in 0..<j { delete(key_schema[k].attribute_name) }
|
|
delete(key_schema)
|
|
delete(gsi.index_name)
|
|
return {}, false
|
|
}
|
|
|
|
key_schema[j] = dynamodb.Key_Schema_Element{
|
|
attribute_name = strings.clone(string(an_str)),
|
|
key_type = kt,
|
|
}
|
|
}
|
|
|
|
// Must have exactly one HASH key
|
|
if hash_count != 1 {
|
|
for ks in key_schema { delete(ks.attribute_name) }
|
|
delete(key_schema)
|
|
delete(gsi.index_name)
|
|
return {}, false
|
|
}
|
|
|
|
gsi.key_schema = key_schema
|
|
|
|
// Projection (optional — defaults to ALL)
|
|
gsi.projection.projection_type = .ALL
|
|
if proj_val, proj_found := obj["Projection"]; proj_found {
|
|
if proj_obj, proj_ok := proj_val.(json.Object); proj_ok {
|
|
if pt_val, pt_found := proj_obj["ProjectionType"]; pt_found {
|
|
if pt_str, pt_ok := pt_val.(json.String); pt_ok {
|
|
switch string(pt_str) {
|
|
case "ALL": gsi.projection.projection_type = .ALL
|
|
case "KEYS_ONLY": gsi.projection.projection_type = .KEYS_ONLY
|
|
case "INCLUDE": gsi.projection.projection_type = .INCLUDE
|
|
}
|
|
}
|
|
}
|
|
|
|
// NonKeyAttributes (only valid for INCLUDE projection)
|
|
if nka_val, nka_found := proj_obj["NonKeyAttributes"]; nka_found {
|
|
if nka_arr, nka_ok := nka_val.(json.Array); nka_ok && len(nka_arr) > 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 strings.clone(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"
|
|
}
|