Skip to content

feat: create item #196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ log = "0.4.22"
futures = "0.3.30"
aws-config = "1.6.1"
aws-sdk-dynamodb = "1.71.2"
base64 = "0.22.1"
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
Expand Down
97 changes: 88 additions & 9 deletions src-tauri/src/dynamo_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use aws_sdk_dynamodb::{Client, config::Credentials, types::AttributeValue};
use aws_config::Region;
use serde::{Deserialize, Serialize};
use serde_json::json;
use base64;

#[derive(Debug, Deserialize)]
pub struct DynamoCredentials {
Expand All @@ -25,6 +26,23 @@ struct ApiResponse {
data: Option<serde_json::Value>,
}

fn convert_json_to_attr_value(value: &serde_json::Value) -> Option<AttributeValue> {
match value {
serde_json::Value::String(s) => Some(AttributeValue::S(s.clone())),
serde_json::Value::Number(n) => Some(AttributeValue::N(n.to_string())),
serde_json::Value::Bool(b) => Some(AttributeValue::Bool(*b)),
serde_json::Value::Null => Some(AttributeValue::Null(true)),
serde_json::Value::Array(arr) => Some(AttributeValue::L(
arr.iter().filter_map(|v| convert_json_to_attr_value(v)).collect()
)),
serde_json::Value::Object(map) => Some(AttributeValue::M(
map.iter().filter_map(|(k, v)| {
convert_json_to_attr_value(v).map(|av| (k.clone(), av))
}).collect()
)),
}
}

#[tauri::command]
pub async fn dynamo_api(
window: tauri::Window,
Expand Down Expand Up @@ -146,18 +164,79 @@ pub async fn dynamo_api(
})
}
},
"put_item" => {
if let Some(_payload) = &options.payload {
// Implementation for put_item would go here
Ok(ApiResponse {
status: 200,
message: "Item put successfully".to_string(),
data: None,
})
"CREATE_ITEM" => {
if let Some(payload) = &options.payload {
// Expecting payload to have an "attributes" array
if let Some(attributes) = payload.get("attributes").and_then(|v| v.as_array()) {
let mut put_item = client.put_item().table_name(&options.table_name);

for attr in attributes {
if let (Some(key), Some(value), Some(attr_type)) = (
attr.get("key").and_then(|v| v.as_str()),
attr.get("value"),
attr.get("type").and_then(|v| v.as_str()),
) {
let attr_value = match attr_type {
"S" => value.as_str().map(|s| AttributeValue::S(s.to_string())),
"N" => value.as_f64().map(|n| AttributeValue::N(n.to_string())),
"B" => value.as_str().map(|s| AttributeValue::B(
aws_sdk_dynamodb::primitives::Blob::new(base64::decode(s).unwrap_or_default())
)),
"BOOL" => value.as_bool().map(AttributeValue::Bool),
"NULL" => Some(AttributeValue::Null(true)),
"SS" => value.as_array().map(|arr| {
AttributeValue::Ss(arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
}),
"NS" => value.as_array().map(|arr| {
AttributeValue::Ns(arr.iter().filter_map(|v| v.as_f64().map(|n| n.to_string())).collect())
}),
"BS" => value.as_array().map(|arr| {
AttributeValue::Bs(arr.iter().filter_map(|v| v.as_str().map(|s| {
aws_sdk_dynamodb::primitives::Blob::new(base64::decode(s).unwrap_or_default())
})).collect())
}),
"L" => value.as_array().map(|arr| {
AttributeValue::L(arr.iter().filter_map(|v| {
// Recursively convert each element
convert_json_to_attr_value(v)
}).collect())
}),
"M" => value.as_object().map(|map| {
AttributeValue::M(map.iter().filter_map(|(k, v)| {
convert_json_to_attr_value(v).map(|av| (k.clone(), av))
}).collect())
}),
_ => None,
};
if let Some(av) = attr_value {
put_item = put_item.item(key, av);
}
}
}

match put_item.send().await {
Ok(_) => Ok(ApiResponse {
status: 200,
message: "Item created successfully".to_string(),
data: None,
}),
Err(e) => Ok(ApiResponse {
status: 500,
message: format!("Failed to create item: {}", e),
data: None,
}),
}
} else {
Ok(ApiResponse {
status: 400,
message: "Attributes array is required".to_string(),
data: None,
})
}
} else {
Ok(ApiResponse {
status: 400,
message: "Item is required for put_item operation".to_string(),
message: "Item payload is required".to_string(),
data: None,
})
}
Expand Down
28 changes: 20 additions & 8 deletions src/components/tool-bar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,30 @@
{{ $t('editor.loadDefault') }}
</n-tooltip>
<n-button-group v-if="props.type === 'DYNAMO_EDITOR'">
<n-button quaternary>
<n-button quaternary @click="handleEditorSwitch('DYNAMO_EDITOR_UI')">
<template #icon>
<n-icon>
<Template />
</n-icon>
</template>
{{ $t('editor.dynamo.uiQuery') }}
</n-button>
<n-button quaternary>
<n-button quaternary @click="handleEditorSwitch('DYNAMO_EDITOR_SQL')">
<template #icon>
<n-icon>
<Code />
</n-icon>
</template>
{{ $t('editor.dynamo.sqlEditor') }}
</n-button>
<n-button quaternary @click="handleEditorSwitch('DYNAMO_EDITOR_CREATE_ITEM')">
<template #icon>
<n-icon>
<Add />
</n-icon>
</template>
{{ $t('editor.dynamo.createItem') }}
</n-button>
</n-button-group>
<n-tabs
v-if="props.type === 'MANAGE'"
Expand All @@ -95,24 +103,22 @@
</template>

<script setup lang="ts">
import { AiStatus, Search, Code, Template } from '@vicons/carbon';
import { Add, AiStatus, Search, Code, Template } from '@vicons/carbon';
import { storeToRefs } from 'pinia';
import { useClusterManageStore, useConnectionStore, useTabStore } from '../store';
import { useLang } from '../lang';
import { CustomError, inputProps } from '../common';

const props = defineProps({ type: String });
const emits = defineEmits(['switch-manage-tab']);

const message = useMessage();
const lang = useLang();

const connectionStore = useConnectionStore();
const { fetchConnections, fetchIndices, selectIndex } = connectionStore;
const { connections } = storeToRefs(connectionStore);

const props = defineProps({
type: String,
});
const emits = defineEmits(['switch-manage-tab']);

const tabStore = useTabStore();
const { loadDefaultSnippet, selectConnection } = tabStore;
const { activePanel, activeElasticsearchIndexOption } = storeToRefs(tabStore);
Expand Down Expand Up @@ -255,6 +261,12 @@ const handleHiddenChange = async (value: boolean) => {
await refreshStates(value);
}
};

const handleEditorSwitch = async (
value: 'DYNAMO_EDITOR_UI' | 'DYNAMO_EDITOR_SQL' | 'DYNAMO_EDITOR_CREATE_ITEM',
) => {
activePanel.value.editorType = value;
};
</script>

<style lang="scss" scoped>
Expand Down
71 changes: 69 additions & 2 deletions src/datasources/dynamoApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,35 @@ export type DynamoIndex = {
};

// Main table info type
export type DynamoDBTableInfo = {
export type RawDynamoDBTableInfo = {
id: string;
name: string;
status: string;
itemCount: number;
sizeBytes: number;
keySchema: KeySchema[]; // Based on connection store usage
attributeDefinitions: AttributeDefinition[];
indices: DynamoIndex[];
creationDateTime: string;
};

export type DynamoDBTableInfo = {
id: string;
name: string;
status: string;
itemCount: number;
sizeBytes: number;
partitionKey: {
name: string;
type: string;
valueType: string;
};
sortKey?: {
name: string;
type: string;
valueType: string;
};
keySchema: KeySchema[];
attributeDefinitions?: AttributeDefinition[];
indices?: DynamoIndex[];
creationDateTime?: string;
Expand Down Expand Up @@ -84,8 +106,27 @@ const dynamoApi = {
if (status !== 200) {
throw new CustomError(status, message);
}
return data as DynamoDBTableInfo;
const { keySchema, attributeDefinitions } = data as RawDynamoDBTableInfo;

const pkName = keySchema.find(({ keyType }) => keyType.toUpperCase() === 'HASH')?.attributeName;
const pkValueType = attributeDefinitions.find(
({ attributeName }) => attributeName === pkName,
)?.attributeType;

const skName = keySchema.find(
({ keyType }) => keyType.toUpperCase() === 'RANGE',
)?.attributeName;
const skValueType = attributeDefinitions.find(
({ attributeName }) => attributeName === skName,
)?.attributeType;

const partitionKey = { name: pkName, valueType: pkValueType, type: 'HASH' };

const sortKey = { name: skName, valueType: skValueType, type: 'RANGE' };

return { ...data, partitionKey, sortKey } as DynamoDBTableInfo;
},

queryTable: async (con: DynamoDBConnection, queryParams: QueryParams): Promise<QueryResult> => {
const credentials = {
region: con.region,
Expand Down Expand Up @@ -136,6 +177,32 @@ const dynamoApi = {

return data as QueryResult;
},
createItem: async (
con: DynamoDBConnection,
attributes: Array<{
key: string;
value: string | number | boolean | null;
type: string;
}>,
) => {
const credentials = {
region: con.region,
access_key_id: con.accessKeyId,
secret_access_key: con.secretAccessKey,
};
const options = {
table_name: con.tableName,
operation: 'CREATE_ITEM',
payload: { attributes },
};

const { status, message, data } = await tauriClient.invokeDynamoApi(credentials, options);

if (status !== 200) {
throw new CustomError(status, message);
}
return data as QueryResult;
},
};

export { dynamoApi };
10 changes: 7 additions & 3 deletions src/lang/enUS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,6 @@ export const enUS = {
unsupportedFile: 'DocKit only supports file end with .search file',
loadDefault: 'Load default code snippet',
dynamo: {
filterKeyRequired: 'Filter key name is required',
filterOperatorRequired: 'Filter operator is required',
filterValueRequired: 'Filter key value is required',
uiQuery: 'Query UI',
sqlEditor: 'PartiQL Editor',
tableOrIndex: 'Table/Index',
Expand All @@ -205,11 +202,18 @@ export const enUS = {
filterTitle: 'Filters - Optional',
inputAttrName: 'Enter attribute name',
inputOperator: 'Select Operator',
type: 'Select attribute type',
inputAttrValue: 'Enter attribute value',
resultTitle: 'Query Result',
indexIsRequired: 'Table/Index is required',
atLeastRequired: 'Partition key or at least 1 filter is required',
scanWarning: 'Request without partitionKey will scan the table',
createItem: 'Create Item',
addAttributesTitle: 'Add Attributes',
attributeNameRequired: 'Attribute name is required',
attributeTypeRequired: 'Attribute type is required',
attributeValueRequired: 'Attribute value is required',
operatorRequired: 'Filter operator is required',
},
},
file: {
Expand Down
10 changes: 7 additions & 3 deletions src/lang/zhCN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,6 @@ export const zhCN = {
unsupportedFile: 'DocKit仅支持以 .search 结尾的文件',
loadDefault: '加载默认代码片段',
dynamo: {
filterKeyRequired: '请输入滤器键名',
filterOperatorRequired: '请输入滤器操作符',
filterValueRequired: '请输入滤器键的值',
uiQuery: '查询 UI',
sqlEditor: 'PartiQL 查询',
tableOrIndex: '表/索引',
Expand All @@ -205,11 +202,18 @@ export const zhCN = {
filterTitle: '条件过滤 - 可选',
inputAttrName: '请输入属性名称',
inputOperator: '选择操作符',
type: '选择属性类型',
inputAttrValue: '请输入属性的值',
resultTitle: '查询结果',
indexIsRequired: '表/索引 是必需的',
atLeastRequired: 'Partition Key 或至少 1 个过滤器是必需的',
scanWarning: '未提供 Partition Key,执行 Scan 操作可能会导致性能问题',
createItem: '添加数据',
addAttributesTitle: '添加属性',
attributeNameRequired: '请输入属性名称',
attributeTypeRequired: '请输入属性类型',
attributeValueRequired: '请输入属性值',
operatorRequired: '请输入滤器操作符',
},
},
file: {
Expand Down
Loading
Loading