use super::{ChannelCol, ALLOWED_COLUMNS};
use helix_core::effect::SqlValue;
pub fn collect_present(data: &serde_json::Value) -> Vec<ChannelCol> {
let mut cols: Vec<ChannelCol> = Vec::new();
const STR_MAP: &[(&str, &str)] = &[
("display_name", "displayName"),
("type", "type"),
("team_id", "teamId"),
("create_by", "createBy"),
("update_by", "updateBy"),
("role", "role"),
("orient", "orient"),
("purpose", "purpose"),
("header", "header"),
("root_id", "rootId"),
("root_post_id", "rootPostId"),
("notify_props", "notifyProps"),
("notify_props", "notify"),
("mention_permission", "mentionPermission"),
("notice_permission", "noticePermission"),
("top_permission", "topPermission"),
("urgent_current_name", "urgentCurrentName"),
("picture_type", "pictureType"),
("unread_post_id", "unreadPostId"),
];
for (col, key) in STR_MAP {
if let Some(v) = data.get(key).and_then(|v| v.as_str()) {
cols.push((col, SqlValue::Text(v.to_string())));
}
}
if let Some(value) = data.get("lastPost").or_else(|| data.get("last_post")).filter(|value| !value.is_null()) {
let post = crate::message_summary::prepare_post(value);
let encoded = post.as_str().map(str::to_owned).unwrap_or_else(|| post.to_string());
cols.push(("last_post", SqlValue::Text(encoded)));
}
const INT_MAP: &[(&str, &str)] = &[
("last_event_seq", "lastEventSeq"),
("last_post_at", "lastPostAt"),
("delete_at", "deleteAt"),
("created_at", "createAt"),
("updated_at", "updateAt"),
("last_root_post_at", "lastRootPostAt"),
("unread_count", "unreadCount"),
("mention_count", "mentionCount"),
("urgent_count", "urgentCount"),
("thread_count", "threadCount"),
("top_count", "topCount"),
("topic_msg_count", "topicMsgCount"),
("admin_max_count", "adminMaxCount"),
("mention_count_root", "mentionCountRoot"),
];
for (col, key) in INT_MAP {
if let Some(v) = data.get(key).and_then(serde_json::Value::as_i64) {
cols.push((col, SqlValue::Integer(v)));
}
}
const BOOL_MAP: &[(&str, &str)] = &[
("is_active", "isActive"),
("is_top", "channelIsTop"),
("has_more", "hasMore"),
("has_urgent_post", "hasUrgentPost"),
("has_schedule_post", "hasSchedulePost"),
];
for (col, key) in BOOL_MAP {
if let Some(v) = data.get(key).and_then(serde_json::Value::as_bool) {
cols.push((col, SqlValue::Integer(v as i64)));
}
}
const JSON_MAP: &[(&str, &str)] = &[
("mention_list", "mentionList"),
("urgent_post_list", "urgentPostList"),
("source", "source"),
("props", "props"),
("picture", "picture"),
("target_users", "targetUsers"),
];
for (col, key) in JSON_MAP {
if let Some(v) = data.get(key).filter(|v| !v.is_null()) {
cols.push((col, SqlValue::Text(v.to_string())));
}
}
cols.retain(|(col, _)| ALLOWED_COLUMNS.contains(col));
cols
}
#[cfg(test)]
mod tests {
use super::*;
fn has(cols: &[ChannelCol], col: &str) -> bool {
cols.iter().any(|(k, _)| *k == col)
}
fn val<'a>(cols: &'a [ChannelCol], col: &str) -> Option<&'a SqlValue> {
cols.iter().find(|(k, _)| *k == col).map(|(_, v)| v)
}
#[test]
fn collect_present_skips_none_fields_s3() {
let data = serde_json::json!({
"displayName": "改名",
"purpose": "简介",
"header": "规则",
"unreadCount": 0, "channelIsTop": true,
"mentionList": ["a"],
});
let cols = collect_present(&data);
assert!(has(&cols, "display_name"));
assert!(matches!(val(&cols, "purpose"), Some(SqlValue::Text(s)) if s == "简介"));
assert!(matches!(val(&cols, "header"), Some(SqlValue::Text(s)) if s == "规则"));
assert!(
matches!(val(&cols, "unread_count"), Some(SqlValue::Integer(0))),
"Some(0) 显式收(清零)"
);
assert!(matches!(val(&cols, "is_top"), Some(SqlValue::Integer(1))));
assert!(matches!(val(&cols, "mention_list"), Some(SqlValue::Text(s)) if s == "[\"a\"]"));
assert!(!has(&cols, "type"));
assert!(!has(&cols, "last_post"));
assert!(!has(&cols, "mention_count"));
}
#[test]
fn collect_present_excludes_member_and_local_cols_s3() {
let data = serde_json::json!({
"owner": { "id": "u1" },
"adminUsers": [{ "id": "u2" }],
"boss": [{ "id": "u3" }],
"memberCount": 9,
"subtopicsLoadedAt": 100,
"displayName": "keep", });
let cols = collect_present(&data);
assert!(has(&cols, "display_name"));
for col in [
"owner",
"admin_users",
"boss",
"member_count",
"subtopics_loaded_at",
] {
assert!(
!has(&cols, col),
"{col} 不进 partial 路径(白名单 / 成员表)"
);
}
}
#[test]
fn collect_present_empty_s3() {
assert!(collect_present(&serde_json::json!({})).is_empty());
}
#[test]
fn collect_present_accepts_notify_only_wire_key() {
let cols = collect_present(&serde_json::json!({"notify": "STRONG"}));
assert!(
matches!(val(&cols, "notify_props"), Some(SqlValue::Text(value)) if value == "STRONG")
);
}
}