Files
windmill/backend/windmill-trigger-postgres/src/mapper.rs
Ruben Fiszel 2eafe6df36 test: add comprehensive test coverage for extracted backend crates
Add 296 tests across unit and integration test suites to cover
the newly extracted crates from the recent refactor commits.

Unit tests (270):
- windmill-trigger-postgres (96): hex codec, bool parsing, type
  conversion, relation tracking, replication message parsing,
  publication data validation
- windmill-trigger-http (92): HMAC signature verification for
  GitHub/Slack/Stripe/TikTok/Twitch/Zoom webhooks, API key auth,
  Basic Auth, route validation, HTTP method/request type serde
- windmill-api-jobs (39): SQL query builder for job listing/counting
  with filters, pagination, label handling
- windmill-trigger (31): TriggerMode serde, query pagination,
  BaseTriggerData backward compat, HandlerAction, ServerState
- windmill-common webhook (7): WebhookMessage serialization tags
- worker nativets/postgresql (5): nativets job execution with
  args/objects/datetime, postgresql query execution

Integration tests (26):
- backend/tests/triggers.rs: capture config CRUD, capture payload
  operations, capture API endpoints, HTTP trigger CRUD with mode
  filtering, all trigger types DB schema validation (websocket,
  kafka, postgres, nats, sqs), schedule operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 08:58:24 +00:00

223 lines
7.2 KiB
Rust

use std::collections::HashMap;
use rust_postgres::types::Type;
use super::Language;
fn postgres_to_typescript_type(postgres_type: Option<Type>) -> String {
let data_type = match postgres_type {
Some(postgres_type) => match postgres_type {
Type::BOOL => "boolean",
Type::BOOL_ARRAY => "Array<boolean>",
Type::CHAR | Type::BPCHAR | Type::VARCHAR | Type::NAME | Type::TEXT => "string",
Type::CHAR_ARRAY
| Type::BPCHAR_ARRAY
| Type::VARCHAR_ARRAY
| Type::NAME_ARRAY
| Type::TEXT_ARRAY => "Array<string>",
Type::INT2 | Type::INT4 | Type::INT8 | Type::NUMERIC => "number",
Type::INT2_ARRAY | Type::INT4_ARRAY | Type::INT8_ARRAY => "Array<number>",
Type::FLOAT4 | Type::FLOAT8 => "number",
Type::FLOAT8_ARRAY | Type::FLOAT4_ARRAY => "Array<number>",
Type::NUMERIC_ARRAY => "Array<number>",
Type::BYTEA => "Array<number>",
Type::BYTEA_ARRAY => "Array<Array<number>>",
Type::DATE => "string",
Type::DATE_ARRAY => "Array<string>",
Type::TIME => "string",
Type::TIME_ARRAY => "Array<string>",
Type::TIMESTAMPTZ | Type::TIMESTAMP => "string",
Type::TIMESTAMPTZ_ARRAY | Type::TIMESTAMP_ARRAY => "Array<string>",
Type::UUID => "string",
Type::UUID_ARRAY => "Array<string>",
Type::JSON | Type::JSONB | Type::JSON_ARRAY | Type::JSONB_ARRAY => "unknown",
Type::OID => "number",
Type::OID_ARRAY => "Array<number>",
_ => "string",
},
None => "string",
};
data_type.to_string()
}
fn into_body_struct(language: Language, mapped_info: Vec<MappingInfo>) -> String {
let mut block = String::new();
match language {
Language::Typescript => {
block.push_str("{\r\n");
for field in mapped_info {
let typescript_type = postgres_to_typescript_type(field.data_type);
let mut key = field.column_name;
if field.is_nullable {
key.push('?');
}
let full_field = format!("\t\t{}: {},\r\n", key, typescript_type);
block.push_str(&full_field);
}
block.push_str("\t}");
}
}
block
}
#[derive(Debug)]
pub struct MappingInfo {
data_type: Option<Type>,
is_nullable: bool,
column_name: String,
}
impl MappingInfo {
pub fn new(column_name: String, data_type: Option<Type>, is_nullable: bool) -> Self {
Self { column_name, data_type, is_nullable }
}
}
pub struct Mapper {
to_template: HashMap<String, HashMap<String, Vec<MappingInfo>>>,
language: Language,
}
impl Mapper {
pub fn new(
to_template: HashMap<String, HashMap<String, Vec<MappingInfo>>>,
language: Language,
) -> Self {
Self { to_template, language }
}
fn into_typescript_template(self) -> Vec<String> {
let mut struct_definitions = Vec::new();
for (_, mapping_info) in self.to_template {
let last_elem = mapping_info.len() - 1;
for (i, (_, mapped_info)) in mapping_info.into_iter().enumerate() {
let mut struct_body = into_body_struct(Language::Typescript, mapped_info);
let struct_body = if i != last_elem {
struct_body.push_str("\r\n");
struct_body
} else {
struct_body
};
struct_definitions.push(struct_body);
}
}
struct_definitions
}
pub fn get_template(self) -> String {
let struct_definition = match self.language {
Language::Typescript => self.into_typescript_template(),
};
let struct_definition = if struct_definition.is_empty() {
"any".to_string()
} else {
struct_definition.join("\t| ")
};
format!(
r#"
export async function main(
transaction_type: "insert" | "update" | "delete",
schema_name: string,
table_name: string,
row: {},
old_row?: {}
) {{
}}
"#,
&struct_definition, &struct_definition
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_postgres_to_typescript_bool() {
assert_eq!(postgres_to_typescript_type(Some(Type::BOOL)), "boolean");
}
#[test]
fn test_postgres_to_typescript_text_types() {
assert_eq!(postgres_to_typescript_type(Some(Type::TEXT)), "string");
assert_eq!(postgres_to_typescript_type(Some(Type::VARCHAR)), "string");
assert_eq!(postgres_to_typescript_type(Some(Type::CHAR)), "string");
}
#[test]
fn test_postgres_to_typescript_number_types() {
assert_eq!(postgres_to_typescript_type(Some(Type::INT2)), "number");
assert_eq!(postgres_to_typescript_type(Some(Type::INT4)), "number");
assert_eq!(postgres_to_typescript_type(Some(Type::INT8)), "number");
assert_eq!(postgres_to_typescript_type(Some(Type::FLOAT4)), "number");
assert_eq!(postgres_to_typescript_type(Some(Type::FLOAT8)), "number");
assert_eq!(postgres_to_typescript_type(Some(Type::NUMERIC)), "number");
}
#[test]
fn test_postgres_to_typescript_array_types() {
assert_eq!(
postgres_to_typescript_type(Some(Type::INT4_ARRAY)),
"Array<number>"
);
assert_eq!(
postgres_to_typescript_type(Some(Type::TEXT_ARRAY)),
"Array<string>"
);
assert_eq!(
postgres_to_typescript_type(Some(Type::BOOL_ARRAY)),
"Array<boolean>"
);
}
#[test]
fn test_postgres_to_typescript_date_types() {
assert_eq!(postgres_to_typescript_type(Some(Type::DATE)), "string");
assert_eq!(postgres_to_typescript_type(Some(Type::TIMESTAMP)), "string");
assert_eq!(postgres_to_typescript_type(Some(Type::TIMESTAMPTZ)), "string");
assert_eq!(postgres_to_typescript_type(Some(Type::UUID)), "string");
}
#[test]
fn test_postgres_to_typescript_json() {
assert_eq!(postgres_to_typescript_type(Some(Type::JSON)), "unknown");
assert_eq!(postgres_to_typescript_type(Some(Type::JSONB)), "unknown");
}
#[test]
fn test_postgres_to_typescript_none() {
assert_eq!(postgres_to_typescript_type(None), "string");
}
#[test]
fn test_into_body_struct_typescript() {
let fields = vec![
MappingInfo::new("id".to_string(), Some(Type::INT4), false),
MappingInfo::new("name".to_string(), Some(Type::TEXT), true),
];
let result = into_body_struct(Language::Typescript, fields);
assert!(result.contains("id: number,"));
assert!(result.contains("name?: string,"));
}
#[test]
fn test_empty_template() {
let mapper = Mapper::new(HashMap::new(), Language::Typescript);
let template = mapper.get_template();
assert!(template.contains("row: any"));
}
#[test]
fn test_mapping_info_nullable_field() {
let info = MappingInfo::new("email".to_string(), Some(Type::VARCHAR), true);
assert!(info.is_nullable);
assert_eq!(info.column_name, "email");
}
}