chore: align sqlite control plane architecture

- replace default Convex control-plane wording with Rust SQLite control-plane across architecture, AGENTS, Reasonix, and design docs

- retire root Convex functions source and deploy script into recycle while keeping explicit cloud/compat/sync-replica boundaries

- add control-plane migration guard/docs and keep CodeGraph refreshed after the SQLite control-plane cutover
This commit is contained in:
lix-2026
2026-05-22 17:45:22 +08:00
parent 531e845600
commit 47e224d419
79 changed files with 7634 additions and 2600 deletions
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "control-plane"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rusqlite = { version = "0.34", features = ["bundled"] }
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
sha2 = "0.10"
hex = "0.4"
[dev-dependencies]
tempfile = "3"
@@ -0,0 +1,172 @@
-- 001-initial-schema.sql
-- MNote control-plane v1 schema
--
-- All tables use TEXT for datetime columns (ISO-8601) so the database
-- remains readable without language-specific timestamp types.
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE,
username TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS auth_identities (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider TEXT NOT NULL,
provider_subject TEXT NOT NULL,
password_hash TEXT,
password_version INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(provider, provider_subject)
);
CREATE TABLE IF NOT EXISTS auth_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE,
user_agent TEXT,
ip_hash TEXT,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
revoked_at TEXT,
last_seen_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_auth_sessions_user ON auth_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_auth_sessions_token ON auth_sessions(token_hash);
CREATE TABLE IF NOT EXISTS workspaces (
id TEXT PRIMARY KEY,
owner_user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
kind TEXT NOT NULL DEFAULT 'personal',
root_uri TEXT NOT NULL UNIQUE,
root_path TEXT NOT NULL,
source_kind TEXT NOT NULL DEFAULT 'local_folder',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_user_id);
CREATE TABLE IF NOT EXISTS workspace_members (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1,
UNIQUE(workspace_id, user_id)
);
CREATE TABLE IF NOT EXISTS directory_grants (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL,
root_uri TEXT NOT NULL,
root_path TEXT NOT NULL,
permission TEXT NOT NULL,
recursive INTEGER NOT NULL DEFAULT 1,
capabilities_json TEXT NOT NULL DEFAULT '[]',
source TEXT NOT NULL DEFAULT 'admin',
status TEXT NOT NULL DEFAULT 'active',
created_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_directory_grants_user ON directory_grants(user_id);
CREATE INDEX IF NOT EXISTS idx_directory_grants_root_uri ON directory_grants(root_uri);
CREATE TABLE IF NOT EXISTS share_links (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
resource_kind TEXT NOT NULL,
resource_id TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
permission TEXT NOT NULL,
created_by TEXT NOT NULL REFERENCES users(id),
expires_at TEXT,
revoked_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_share_links_resource ON share_links(workspace_id, resource_kind, resource_id);
CREATE TABLE IF NOT EXISTS sync_state (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
remote_kind TEXT NOT NULL,
remote_id TEXT,
cursor TEXT,
last_synced_at TEXT,
status TEXT NOT NULL DEFAULT 'idle',
error_json TEXT,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1,
UNIQUE(workspace_id, remote_kind)
);
CREATE TABLE IF NOT EXISTS ai_policies (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
workspace_id TEXT REFERENCES workspaces(id) ON DELETE CASCADE,
allowed_roots_json TEXT NOT NULL DEFAULT '[]',
model_policy_json TEXT NOT NULL DEFAULT '{}',
quota_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
actor_user_id TEXT REFERENCES users(id),
action TEXT NOT NULL,
target_kind TEXT NOT NULL,
target_id TEXT,
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log(actor_user_id, created_at);
CREATE INDEX IF NOT EXISTS idx_audit_target ON audit_log(target_kind, target_id, created_at);
CREATE TABLE IF NOT EXISTS outbox_events (
id TEXT PRIMARY KEY,
topic TEXT NOT NULL,
event_type TEXT NOT NULL,
payload_json TEXT NOT NULL,
created_at TEXT NOT NULL,
delivered_at TEXT,
attempts INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_outbox_pending ON outbox_events(delivered_at, created_at);
CREATE TABLE IF NOT EXISTS legacy_id_map (
id TEXT PRIMARY KEY,
legacy_system TEXT NOT NULL,
legacy_kind TEXT NOT NULL,
legacy_id TEXT NOT NULL,
new_kind TEXT NOT NULL,
new_id TEXT NOT NULL,
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
UNIQUE(legacy_system, legacy_kind, legacy_id)
);
@@ -0,0 +1,48 @@
-- 002-ai-runtime-store.sql
-- MNote ACP/Hermes runtime session store.
CREATE TABLE IF NOT EXISTS ai_runtime_runs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
workspace_id TEXT,
document_id TEXT,
session_id TEXT NOT NULL,
run_id TEXT NOT NULL UNIQUE,
title TEXT,
profile TEXT NOT NULL,
acp_runtime TEXT NOT NULL,
trace_id TEXT,
status TEXT NOT NULL,
runtime_json TEXT NOT NULL DEFAULT '{}',
payload_json TEXT NOT NULL DEFAULT '{}',
deleted_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_ai_runtime_runs_user_session
ON ai_runtime_runs(user_id, session_id, updated_at);
CREATE INDEX IF NOT EXISTS idx_ai_runtime_runs_workspace
ON ai_runtime_runs(workspace_id, updated_at);
CREATE INDEX IF NOT EXISTS idx_ai_runtime_runs_document
ON ai_runtime_runs(document_id, updated_at);
CREATE TABLE IF NOT EXISTS ai_runtime_events (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
workspace_id TEXT,
document_id TEXT,
session_id TEXT NOT NULL,
run_id TEXT NOT NULL,
profile TEXT NOT NULL,
acp_runtime TEXT NOT NULL,
event_type TEXT NOT NULL,
payload_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_ai_runtime_events_run
ON ai_runtime_events(user_id, run_id, created_at);
CREATE INDEX IF NOT EXISTS idx_ai_runtime_events_session
ON ai_runtime_events(user_id, session_id, created_at);
+38
View File
@@ -0,0 +1,38 @@
use std::fmt;
#[derive(Debug, Clone)]
pub enum ControlPlaneError {
NotFound(String),
Conflict(String),
InvalidInput(String),
Storage(String),
Unauthorized(String),
SessionExpired(String),
}
impl fmt::Display for ControlPlaneError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ControlPlaneError::NotFound(msg) => write!(f, "NotFound: {msg}"),
ControlPlaneError::Conflict(msg) => write!(f, "Conflict: {msg}"),
ControlPlaneError::InvalidInput(msg) => write!(f, "InvalidInput: {msg}"),
ControlPlaneError::Storage(msg) => write!(f, "Storage: {msg}"),
ControlPlaneError::Unauthorized(msg) => write!(f, "Unauthorized: {msg}"),
ControlPlaneError::SessionExpired(msg) => write!(f, "SessionExpired: {msg}"),
}
}
}
impl std::error::Error for ControlPlaneError {}
impl From<rusqlite::Error> for ControlPlaneError {
fn from(e: rusqlite::Error) -> Self {
ControlPlaneError::Storage(e.to_string())
}
}
impl From<serde_json::Error> for ControlPlaneError {
fn from(e: serde_json::Error) -> Self {
ControlPlaneError::Storage(format!("JSON error: {e}"))
}
}
+11
View File
@@ -0,0 +1,11 @@
pub mod error;
pub mod migrations;
pub mod model;
pub mod sqlite;
pub mod store;
pub use error::ControlPlaneError;
pub use migrations::run_migrations;
pub use model::*;
pub use sqlite::SqliteControlPlaneStore;
pub use store::*;
+109
View File
@@ -0,0 +1,109 @@
//! SQLite schema migrations for the MNote control-plane database.
//!
//! Each migration is a named step that runs inside a transaction.
//! The `migrations` table tracks which steps have been applied.
use rusqlite::{Connection, Result as SqlResult};
const MIGRATIONS: &[(&str, &str)] = &[
(
"v1-initial-schema",
include_str!("../migrations/001-initial-schema.sql"),
),
(
"v2-ai-runtime-store",
include_str!("../migrations/002-ai-runtime-store.sql"),
),
];
/// Create the `_migrations` meta-table if it does not exist.
fn ensure_meta_table(conn: &Connection) -> SqlResult<()> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS _migrations (
name TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
)
}
/// Run all pending migrations inside a single transaction.
pub fn run_migrations(conn: &Connection) -> SqlResult<()> {
ensure_meta_table(conn)?;
let already_applied: Vec<String> = conn
.prepare("SELECT name FROM _migrations")?
.query_map([], |row| row.get(0))?
.filter_map(|r| r.ok())
.collect();
for (name, sql) in MIGRATIONS {
if already_applied.contains(&name.to_string()) {
continue;
}
tracing::info!("Running migration: {name}");
conn.execute_batch(sql)?;
conn.execute("INSERT INTO _migrations (name) VALUES (?1)", [name])?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
/// Create a fresh in-memory SQLite database and run migrations.
fn fresh_db() -> Connection {
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch(
"PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA foreign_keys=ON;",
)
.expect("pragmas");
conn
}
fn table_exists(conn: &Connection, name: &str) -> bool {
conn.query_row(
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?1",
[name],
|row| row.get::<_, i64>(0),
)
.unwrap_or(0)
> 0
}
#[test]
fn migration_is_idempotent() {
let conn = fresh_db();
run_migrations(&conn).expect("first run");
run_migrations(&conn).expect("second run (idempotent)");
// All expected tables exist
for table in &[
"users",
"auth_identities",
"auth_sessions",
"workspaces",
"workspace_members",
"directory_grants",
"share_links",
"sync_state",
"ai_policies",
"audit_log",
"outbox_events",
"legacy_id_map",
"ai_runtime_runs",
"ai_runtime_events",
] {
assert!(table_exists(&conn, table), "table {table} should exist");
}
}
#[test]
fn migration_runs_fresh_db_ok() {
let conn = fresh_db();
run_migrations(&conn).expect("fresh migration ok");
assert!(table_exists(&conn, "users"));
assert!(table_exists(&conn, "_migrations"));
}
}
+348
View File
@@ -0,0 +1,348 @@
//! SQLite 控制面 Phase 1 的最小数据模型。
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub type EntityId = String;
pub type Timestamp = String;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserRecord {
pub id: EntityId,
pub email: Option<String>,
pub username: String,
pub display_name: String,
pub role: String,
pub status: String,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub revision: i64,
}
pub fn session_token_hash(raw_token: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(raw_token.as_bytes());
hex::encode(hasher.finalize())
}
pub fn password_hash_v1(password: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(b"mnote-password-v1:");
hasher.update(password.as_bytes());
format!("sha256-v1:{}", hex::encode(hasher.finalize()))
}
pub fn share_token_hash_v1(token: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(b"mnote-share-token-v1:");
hasher.update(token.as_bytes());
format!("sha256-v1:{}", hex::encode(hasher.finalize()))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpsertUserInput {
pub id: Option<EntityId>,
pub email: Option<String>,
pub username: String,
pub display_name: String,
pub role: Option<String>,
pub password_hash: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreatePasswordIdentityInput {
pub user_id: EntityId,
pub email: Option<String>,
pub username: String,
pub password: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthenticatePasswordInput {
pub account: String,
pub password: String,
pub session_id: Option<EntityId>,
pub token_hash: String,
pub user_agent: Option<String>,
pub ip_hash: Option<String>,
pub expires_at: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthSessionRecord {
pub id: EntityId,
pub user_id: EntityId,
pub token_hash: String,
pub user_agent: Option<String>,
pub ip_hash: Option<String>,
pub created_at: Timestamp,
pub expires_at: Timestamp,
pub revoked_at: Option<Timestamp>,
pub last_seen_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateSessionInput {
pub id: Option<EntityId>,
pub user_id: EntityId,
pub token_hash: String,
pub user_agent: Option<String>,
pub ip_hash: Option<String>,
pub expires_at: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedAuthSession {
pub session: AuthSessionRecord,
pub user: UserRecord,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkspaceRecord {
pub id: EntityId,
pub owner_user_id: EntityId,
pub name: String,
pub kind: String,
pub root_uri: String,
pub root_path: String,
pub source_kind: String,
pub status: String,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub revision: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectoryGrantRecord {
pub id: EntityId,
pub user_id: EntityId,
pub workspace_id: Option<EntityId>,
pub root_uri: String,
pub root_path: String,
pub permission: String,
pub recursive: bool,
pub capabilities_json: String,
pub source: String,
pub status: String,
pub created_by: Option<EntityId>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub revision: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectoryGrantLookup {
pub grant_id: Option<EntityId>,
pub user_id: Option<EntityId>,
pub root_uri: Option<String>,
pub include_revoked: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectoryGrantInput {
pub user_id: EntityId,
pub workspace_id: Option<EntityId>,
pub root_uri: String,
pub root_path: String,
pub permission: String,
pub recursive: bool,
pub capabilities: Vec<String>,
pub source: String,
pub created_by: Option<EntityId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedAccess {
pub user_id: EntityId,
pub root_uri: String,
pub permission: String,
pub grant_ids: Vec<EntityId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutboxEventRecord {
pub id: EntityId,
pub topic: String,
pub event_type: String,
pub payload_json: String,
pub created_at: Timestamp,
pub delivered_at: Option<Timestamp>,
pub attempts: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutboxEventInput {
pub topic: String,
pub event_type: String,
pub payload_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AiPolicyRecord {
pub id: EntityId,
pub user_id: Option<EntityId>,
pub workspace_id: Option<EntityId>,
pub allowed_roots_json: String,
pub model_policy_json: String,
pub quota_json: String,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub revision: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpsertAiPolicyInput {
pub id: Option<EntityId>,
pub user_id: Option<EntityId>,
pub workspace_id: Option<EntityId>,
pub allowed_roots_json: String,
pub model_policy_json: String,
pub quota_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncStateRecord {
pub id: EntityId,
pub workspace_id: EntityId,
pub remote_kind: String,
pub remote_id: Option<String>,
pub cursor: Option<String>,
pub last_synced_at: Option<Timestamp>,
pub status: String,
pub error_json: Option<String>,
pub updated_at: Timestamp,
pub revision: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpsertSyncStateInput {
pub id: Option<EntityId>,
pub workspace_id: EntityId,
pub remote_kind: String,
pub remote_id: Option<String>,
pub cursor: Option<String>,
pub last_synced_at: Option<Timestamp>,
pub status: String,
pub error_json: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AiRuntimeRunRecord {
pub id: EntityId,
pub user_id: EntityId,
pub workspace_id: Option<EntityId>,
pub document_id: Option<String>,
pub session_id: EntityId,
pub run_id: EntityId,
pub title: Option<String>,
pub profile: String,
pub acp_runtime: String,
pub trace_id: Option<String>,
pub status: String,
pub runtime_json: String,
pub payload_json: String,
pub deleted_at: Option<Timestamp>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub revision: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpsertAiRuntimeRunInput {
pub id: Option<EntityId>,
pub user_id: EntityId,
pub workspace_id: Option<EntityId>,
pub document_id: Option<String>,
pub session_id: EntityId,
pub run_id: EntityId,
pub title: Option<String>,
pub profile: String,
pub acp_runtime: String,
pub trace_id: Option<String>,
pub status: String,
pub runtime_json: String,
pub payload_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AiRuntimeEventRecord {
pub id: EntityId,
pub user_id: EntityId,
pub workspace_id: Option<EntityId>,
pub document_id: Option<String>,
pub session_id: EntityId,
pub run_id: EntityId,
pub profile: String,
pub acp_runtime: String,
pub event_type: String,
pub payload_json: String,
pub created_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppendAiRuntimeEventInput {
pub id: Option<EntityId>,
pub user_id: EntityId,
pub workspace_id: Option<EntityId>,
pub document_id: Option<String>,
pub session_id: EntityId,
pub run_id: EntityId,
pub profile: String,
pub acp_runtime: String,
pub event_type: String,
pub payload_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShareLinkRecord {
pub id: EntityId,
pub workspace_id: EntityId,
pub resource_kind: String,
pub resource_id: String,
pub token_hash: String,
pub permission: String,
pub created_by: EntityId,
pub expires_at: Option<Timestamp>,
pub revoked_at: Option<Timestamp>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub revision: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateShareLinkInput {
pub id: Option<EntityId>,
pub workspace_id: EntityId,
pub resource_kind: String,
pub resource_id: String,
pub token: Option<String>,
pub permission: String,
pub created_by: EntityId,
pub expires_at: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreatedShareLink {
pub link: ShareLinkRecord,
pub token: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditLogRecord {
pub id: EntityId,
pub actor_user_id: Option<EntityId>,
pub action: String,
pub target_kind: String,
pub target_id: Option<EntityId>,
pub metadata_json: String,
pub created_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppendAuditInput {
pub actor_user_id: Option<EntityId>,
pub action: String,
pub target_kind: String,
pub target_id: Option<EntityId>,
pub metadata_json: String,
}
File diff suppressed because it is too large Load Diff
+172
View File
@@ -0,0 +1,172 @@
//! 控制面 store trait。
use crate::error::ControlPlaneError;
use crate::model::{
AiPolicyRecord, AiRuntimeEventRecord, AiRuntimeRunRecord, AppendAiRuntimeEventInput,
AppendAuditInput, AuditLogRecord, AuthSessionRecord, AuthenticatePasswordInput,
CreatePasswordIdentityInput, CreateSessionInput, CreateShareLinkInput, CreatedShareLink,
DirectoryGrantInput, DirectoryGrantLookup, DirectoryGrantRecord, OutboxEventInput,
OutboxEventRecord, ResolvedAccess, ResolvedAuthSession, ShareLinkRecord, SyncStateRecord,
UpsertAiPolicyInput, UpsertAiRuntimeRunInput, UpsertSyncStateInput, UpsertUserInput,
UserRecord, WorkspaceRecord,
};
pub trait ControlPlaneStore: Send + Sync {
fn upsert_user(&self, input: UpsertUserInput) -> Result<UserRecord, ControlPlaneError>;
fn create_password_identity(
&self,
input: CreatePasswordIdentityInput,
) -> Result<(), ControlPlaneError>;
fn authenticate_password(
&self,
input: AuthenticatePasswordInput,
) -> Result<ResolvedAuthSession, ControlPlaneError>;
fn create_session(
&self,
input: CreateSessionInput,
) -> Result<AuthSessionRecord, ControlPlaneError>;
fn get_session_by_token_hash(
&self,
token_hash: &str,
) -> Result<Option<ResolvedAuthSession>, ControlPlaneError>;
fn revoke_session(&self, session_id: &str) -> Result<(), ControlPlaneError>;
fn ensure_default_workspace(
&self,
actor_id: &str,
) -> Result<WorkspaceRecord, ControlPlaneError>;
fn grant_directory_access(
&self,
input: DirectoryGrantInput,
) -> Result<DirectoryGrantRecord, ControlPlaneError>;
fn list_directory_grants(&self) -> Result<Vec<DirectoryGrantRecord>, ControlPlaneError>;
fn list_directory_grants_for_actor(
&self,
actor_id: &str,
) -> Result<Vec<DirectoryGrantRecord>, ControlPlaneError>;
fn find_directory_grants(
&self,
lookup: DirectoryGrantLookup,
) -> Result<Vec<DirectoryGrantRecord>, ControlPlaneError>;
fn revoke_directory_grant(
&self,
grant_id: &str,
expected_revision: Option<i64>,
) -> Result<(), ControlPlaneError>;
fn resolve_access(
&self,
actor_id: &str,
root_uri: &str,
) -> Result<ResolvedAccess, ControlPlaneError>;
fn create_share_link(
&self,
input: CreateShareLinkInput,
) -> Result<CreatedShareLink, ControlPlaneError>;
fn list_share_links(
&self,
workspace_id: &str,
) -> Result<Vec<ShareLinkRecord>, ControlPlaneError>;
fn resolve_share_link(
&self,
token_hash: &str,
) -> Result<Option<ShareLinkRecord>, ControlPlaneError>;
fn revoke_share_link(&self, link_id: &str) -> Result<(), ControlPlaneError>;
fn append_audit(&self, input: AppendAuditInput) -> Result<(), ControlPlaneError>;
fn list_audit_log(&self, limit: usize) -> Result<Vec<AuditLogRecord>, ControlPlaneError>;
fn append_outbox(
&self,
input: OutboxEventInput,
) -> Result<OutboxEventRecord, ControlPlaneError>;
fn drain_outbox(&self, limit: usize) -> Result<Vec<OutboxEventRecord>, ControlPlaneError>;
fn mark_outbox_delivered(&self, event_id: &str) -> Result<(), ControlPlaneError>;
fn upsert_ai_policy(
&self,
input: UpsertAiPolicyInput,
) -> Result<AiPolicyRecord, ControlPlaneError>;
fn get_ai_policy(
&self,
actor_id: &str,
workspace_id: Option<&str>,
) -> Result<Option<AiPolicyRecord>, ControlPlaneError>;
fn upsert_sync_state(
&self,
input: UpsertSyncStateInput,
) -> Result<SyncStateRecord, ControlPlaneError>;
fn get_sync_state(
&self,
workspace_id: &str,
remote_kind: &str,
) -> Result<Option<SyncStateRecord>, ControlPlaneError>;
fn upsert_ai_runtime_run(
&self,
input: UpsertAiRuntimeRunInput,
) -> Result<AiRuntimeRunRecord, ControlPlaneError>;
fn list_ai_runtime_runs(
&self,
user_id: &str,
workspace_id: Option<&str>,
document_id: Option<&str>,
session_id: Option<&str>,
limit: usize,
) -> Result<Vec<AiRuntimeRunRecord>, ControlPlaneError>;
fn append_ai_runtime_event(
&self,
input: AppendAiRuntimeEventInput,
) -> Result<AiRuntimeEventRecord, ControlPlaneError>;
fn list_ai_runtime_events(
&self,
user_id: &str,
run_id: &str,
limit: usize,
) -> Result<Vec<AiRuntimeEventRecord>, ControlPlaneError>;
fn rename_ai_runtime_session(
&self,
user_id: &str,
session_id: &str,
workspace_id: Option<&str>,
title: &str,
) -> Result<Vec<AiRuntimeRunRecord>, ControlPlaneError>;
fn auto_title_ai_runtime_session(
&self,
user_id: &str,
session_id: &str,
workspace_id: Option<&str>,
) -> Result<Option<AiRuntimeRunRecord>, ControlPlaneError>;
fn delete_ai_runtime_session(
&self,
user_id: &str,
session_id: &str,
workspace_id: Option<&str>,
) -> Result<usize, ControlPlaneError>;
}