use omnigraph::db::{ GraphCommit, MergeOutcome, ReadTarget, RunRecord, SchemaApplyResult, Snapshot, }; use omnigraph::error::{MergeConflict, MergeConflictKind}; use omnigraph::loader::{IngestResult, LoadMode}; use omnigraph_compiler::SchemaMigrationStep; use omnigraph_compiler::result::QueryResult; use serde::{Deserialize, Serialize}; use serde_json::Value; use utoipa::{IntoParams, ToSchema}; /// Shadow enum for documenting [`LoadMode`] in the OpenAPI schema. #[derive(ToSchema)] #[schema(as = LoadMode)] #[allow(dead_code)] enum LoadModeSchema { /// Overwrite existing data. #[schema(rename = "overwrite")] Overwrite, /// Append to existing data. #[schema(rename = "append")] Append, /// Merge by id key (upsert). #[schema(rename = "merge")] Merge, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct SnapshotTableOutput { pub table_key: String, pub table_path: String, pub table_version: u64, pub table_branch: Option, pub row_count: u64, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct SnapshotOutput { pub branch: String, pub manifest_version: u64, pub tables: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct RunOutput { pub run_id: String, pub target_branch: String, pub run_branch: String, pub base_snapshot_id: String, pub base_manifest_version: u64, pub operation_hash: Option, pub actor_id: Option, pub status: String, pub published_snapshot_id: Option, /// Run creation time as Unix epoch microseconds. #[schema(example = 1714000000000000i64)] pub created_at: i64, /// Last status change as Unix epoch microseconds. #[schema(example = 1714000000000000i64)] pub updated_at: i64, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct RunListOutput { pub runs: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct BranchCreateRequest { /// Parent branch to fork from. Defaults to `main`. pub from: Option, /// Name of the new branch. Must not already exist. pub name: String, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct BranchCreateOutput { pub uri: String, pub from: String, pub name: String, pub actor_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct BranchListOutput { pub branches: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct BranchDeleteOutput { pub uri: String, pub name: String, pub actor_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct BranchMergeRequest { /// Source branch whose commits will be merged. pub source: String, /// Target branch that will receive the merge. Defaults to `main`. pub target: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum BranchMergeOutcome { AlreadyUpToDate, FastForward, Merged, } impl From for BranchMergeOutcome { fn from(value: MergeOutcome) -> Self { match value { MergeOutcome::AlreadyUpToDate => Self::AlreadyUpToDate, MergeOutcome::FastForward => Self::FastForward, MergeOutcome::Merged => Self::Merged, } } } impl BranchMergeOutcome { pub fn as_str(self) -> &'static str { match self { Self::AlreadyUpToDate => "already_up_to_date", Self::FastForward => "fast_forward", Self::Merged => "merged", } } } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct BranchMergeOutput { pub source: String, pub target: String, pub outcome: BranchMergeOutcome, pub actor_id: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum MergeConflictKindOutput { DivergentInsert, DivergentUpdate, DeleteVsUpdate, OrphanEdge, UniqueViolation, CardinalityViolation, ValueConstraintViolation, } impl MergeConflictKindOutput { pub fn as_str(self) -> &'static str { match self { Self::DivergentInsert => "divergent_insert", Self::DivergentUpdate => "divergent_update", Self::DeleteVsUpdate => "delete_vs_update", Self::OrphanEdge => "orphan_edge", Self::UniqueViolation => "unique_violation", Self::CardinalityViolation => "cardinality_violation", Self::ValueConstraintViolation => "value_constraint_violation", } } } impl From for MergeConflictKindOutput { fn from(value: MergeConflictKind) -> Self { match value { MergeConflictKind::DivergentInsert => Self::DivergentInsert, MergeConflictKind::DivergentUpdate => Self::DivergentUpdate, MergeConflictKind::DeleteVsUpdate => Self::DeleteVsUpdate, MergeConflictKind::OrphanEdge => Self::OrphanEdge, MergeConflictKind::UniqueViolation => Self::UniqueViolation, MergeConflictKind::CardinalityViolation => Self::CardinalityViolation, MergeConflictKind::ValueConstraintViolation => Self::ValueConstraintViolation, } } } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct MergeConflictOutput { pub table_key: String, pub row_id: Option, pub kind: MergeConflictKindOutput, pub message: String, } impl From<&MergeConflict> for MergeConflictOutput { fn from(value: &MergeConflict) -> Self { Self { table_key: value.table_key.clone(), row_id: value.row_id.clone(), kind: value.kind.into(), message: value.message.clone(), } } } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ReadTargetOutput { pub branch: Option, pub snapshot: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ReadOutput { pub query_name: String, pub target: ReadTargetOutput, pub row_count: usize, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub columns: Vec, pub rows: Value, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ChangeOutput { pub branch: String, pub query_name: String, pub affected_nodes: usize, pub affected_edges: usize, pub actor_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct IngestTableOutput { pub table_key: String, pub rows_loaded: usize, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct IngestOutput { pub uri: String, pub branch: String, pub base_branch: String, pub branch_created: bool, #[schema(value_type = LoadModeSchema)] pub mode: LoadMode, pub tables: Vec, pub actor_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct CommitOutput { pub graph_commit_id: String, pub manifest_branch: Option, pub manifest_version: u64, pub parent_commit_id: Option, pub merged_parent_commit_id: Option, pub actor_id: Option, /// Commit creation time as Unix epoch microseconds. #[schema(example = 1714000000000000i64)] pub created_at: i64, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct CommitListOutput { pub commits: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ReadRequest { /// GQ query source. May declare one or more named queries; pick one with /// `query_name` if there is more than one. #[schema(example = "query get_person($name: String) {\n match {\n $p: Person { name: $name }\n }\n return { $p.name, $p.age }\n}")] pub query_source: String, /// Name of the query to run when `query_source` declares multiple. Optional /// when only one query is declared. pub query_name: Option, /// JSON object whose keys match the query's declared parameters. pub params: Option, /// Branch to read from. Mutually exclusive with `snapshot`. Defaults to `main`. pub branch: Option, /// Snapshot id to read from. Mutually exclusive with `branch`. pub snapshot: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ChangeRequest { /// GQ mutation source containing `insert`, `update`, or `delete` statements. /// May declare multiple named mutations; pick one with `query_name`. #[schema(example = "query insert_person($name: String, $age: I32) {\n insert Person { name: $name, age: $age }\n}")] pub query_source: String, /// Name of the mutation to run when `query_source` declares multiple. pub query_name: Option, /// JSON object whose keys match the mutation's declared parameters. pub params: Option, /// Target branch. Defaults to `main`. pub branch: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct SchemaApplyRequest { /// Project schema in `.pg` source form. The diff against the current /// schema produces the migration steps that will be applied. #[schema(example = "node Person {\n name: String @key\n age: I32?\n}\n\nedge Knows: Person -> Person")] pub schema_source: String, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct SchemaApplyOutput { pub uri: String, pub supported: bool, pub applied: bool, pub step_count: usize, pub manifest_version: u64, #[schema(value_type = Vec)] pub steps: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct SchemaOutput { pub schema_source: String, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct IngestRequest { /// Target branch. Created from `from` if it does not yet exist. Defaults to `main`. pub branch: Option, /// Parent branch used to create `branch` if it does not exist. Defaults to `main`. pub from: Option, /// How existing rows are handled. Defaults to `merge`. #[schema(value_type = Option)] pub mode: Option, /// NDJSON payload: one record per line, each shaped /// `{"type": "", "data": {...}}`. #[schema(example = "{\"type\": \"Person\", \"data\": {\"name\": \"Alice\", \"age\": 30}}\n{\"type\": \"Person\", \"data\": {\"name\": \"Bob\", \"age\": 25}}")] pub data: String, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ExportRequest { /// Branch to export. Defaults to `main`. pub branch: Option, /// Restrict the export to these node/edge type names. Empty exports all types. #[serde(default)] pub type_names: Vec, /// Restrict the export to these table keys. Empty exports all tables. #[serde(default)] pub table_keys: Vec, } #[derive(Debug, Clone, Deserialize, IntoParams)] pub struct SnapshotQuery { pub branch: Option, } #[derive(Debug, Clone, Deserialize, IntoParams)] pub struct CommitListQuery { pub branch: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct HealthOutput { pub status: String, pub version: String, #[serde(skip_serializing_if = "Option::is_none")] pub source_version: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum ErrorCode { Unauthorized, Forbidden, BadRequest, NotFound, Conflict, Internal, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct ErrorOutput { pub error: String, #[serde(skip_serializing_if = "Option::is_none")] pub code: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub merge_conflicts: Vec, } pub fn snapshot_payload(branch: &str, snapshot: &Snapshot) -> SnapshotOutput { let mut entries: Vec<_> = snapshot.entries().cloned().collect(); entries.sort_by(|a, b| a.table_key.cmp(&b.table_key)); let tables = entries .iter() .map(|entry| SnapshotTableOutput { table_key: entry.table_key.clone(), table_path: entry.table_path.clone(), table_version: entry.table_version, table_branch: entry.table_branch.clone(), row_count: entry.row_count, }) .collect::>(); SnapshotOutput { branch: branch.to_string(), manifest_version: snapshot.version(), tables, } } pub fn schema_apply_output(uri: &str, result: SchemaApplyResult) -> SchemaApplyOutput { SchemaApplyOutput { uri: uri.to_string(), supported: result.supported, applied: result.applied, step_count: result.steps.len(), manifest_version: result.manifest_version, steps: result.steps, } } pub fn run_output(run: &RunRecord) -> RunOutput { RunOutput { run_id: run.run_id.as_str().to_string(), target_branch: run.target_branch.clone(), run_branch: run.run_branch.clone(), base_snapshot_id: run.base_snapshot_id.as_str().to_string(), base_manifest_version: run.base_manifest_version, operation_hash: run.operation_hash.clone(), actor_id: run.actor_id.clone(), status: run.status.as_str().to_string(), published_snapshot_id: run.published_snapshot_id.clone(), created_at: run.created_at, updated_at: run.updated_at, } } pub fn commit_output(commit: &GraphCommit) -> CommitOutput { CommitOutput { graph_commit_id: commit.graph_commit_id.clone(), manifest_branch: commit.manifest_branch.clone(), manifest_version: commit.manifest_version, parent_commit_id: commit.parent_commit_id.clone(), merged_parent_commit_id: commit.merged_parent_commit_id.clone(), actor_id: commit.actor_id.clone(), created_at: commit.created_at, } } pub fn read_output(query_name: String, target: &ReadTarget, result: QueryResult) -> ReadOutput { let columns = result .schema() .fields() .iter() .map(|field| field.name().clone()) .collect(); ReadOutput { query_name, target: read_target_output(target), row_count: result.num_rows(), columns, rows: result.to_rust_json(), } } pub fn ingest_output(uri: &str, result: &IngestResult, actor_id: Option) -> IngestOutput { IngestOutput { uri: uri.to_string(), branch: result.branch.clone(), base_branch: result.base_branch.clone(), branch_created: result.branch_created, mode: result.mode, tables: result .tables .iter() .map(|table| IngestTableOutput { table_key: table.table_key.clone(), rows_loaded: table.rows_loaded, }) .collect(), actor_id, } } pub fn read_target_output(target: &ReadTarget) -> ReadTargetOutput { match target { ReadTarget::Branch(branch) => ReadTargetOutput { branch: Some(branch.clone()), snapshot: None, }, ReadTarget::Snapshot(snapshot) => ReadTargetOutput { branch: None, snapshot: Some(snapshot.as_str().to_string()), }, } }