mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge upstream/main
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
CREATE TABLE IF NOT EXISTS routing_groups (
|
||||
`id` VARCHAR(64) NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` LONGTEXT,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`is_system_default` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`config_json` JSON NOT NULL,
|
||||
`version` BIGINT NOT NULL DEFAULT 1,
|
||||
`created_at` BIGINT NOT NULL,
|
||||
`updated_at` BIGINT NOT NULL,
|
||||
`published_at` BIGINT,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY routing_groups_name_key (`name`),
|
||||
KEY routing_groups_system_default_idx (`is_system_default`, `enabled`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_group_bindings (
|
||||
`id` VARCHAR(64) NOT NULL,
|
||||
`group_id` VARCHAR(64) NOT NULL,
|
||||
`subject_type` VARCHAR(32) NOT NULL,
|
||||
`subject_id` VARCHAR(64) NOT NULL,
|
||||
`is_default` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`allow_explicit_select` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` BIGINT NOT NULL,
|
||||
`updated_at` BIGINT NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY routing_group_bindings_group_id_idx (`group_id`),
|
||||
KEY routing_group_bindings_subject_idx (`subject_type`, `subject_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_group_versions (
|
||||
`id` VARCHAR(64) NOT NULL,
|
||||
`group_id` VARCHAR(64) NOT NULL,
|
||||
`version` BIGINT NOT NULL,
|
||||
`config_json` JSON NOT NULL,
|
||||
`created_at` BIGINT NOT NULL,
|
||||
`created_by` VARCHAR(64),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY routing_group_versions_group_version_key (`group_id`, `version`),
|
||||
KEY routing_group_versions_group_id_idx (`group_id`)
|
||||
);
|
||||
@@ -0,0 +1,82 @@
|
||||
CREATE TABLE IF NOT EXISTS public.routing_groups (
|
||||
id character varying(64) NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
description text,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
is_system_default boolean DEFAULT false NOT NULL,
|
||||
config_json jsonb NOT NULL,
|
||||
version bigint DEFAULT 1 NOT NULL,
|
||||
created_at bigint NOT NULL,
|
||||
updated_at bigint NOT NULL,
|
||||
published_at bigint
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'routing_groups_pkey'
|
||||
) THEN
|
||||
ALTER TABLE ONLY public.routing_groups
|
||||
ADD CONSTRAINT routing_groups_pkey PRIMARY KEY (id);
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'routing_groups_name_key'
|
||||
) THEN
|
||||
ALTER TABLE ONLY public.routing_groups
|
||||
ADD CONSTRAINT routing_groups_name_key UNIQUE (name);
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE INDEX IF NOT EXISTS routing_groups_system_default_idx
|
||||
ON public.routing_groups USING btree (is_system_default, enabled);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.routing_group_bindings (
|
||||
id character varying(64) NOT NULL,
|
||||
group_id character varying(64) NOT NULL,
|
||||
subject_type character varying(32) NOT NULL,
|
||||
subject_id character varying(64) NOT NULL,
|
||||
is_default boolean DEFAULT false NOT NULL,
|
||||
allow_explicit_select boolean DEFAULT true NOT NULL,
|
||||
created_at bigint NOT NULL,
|
||||
updated_at bigint NOT NULL
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'routing_group_bindings_pkey'
|
||||
) THEN
|
||||
ALTER TABLE ONLY public.routing_group_bindings
|
||||
ADD CONSTRAINT routing_group_bindings_pkey PRIMARY KEY (id);
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE INDEX IF NOT EXISTS routing_group_bindings_group_id_idx
|
||||
ON public.routing_group_bindings USING btree (group_id);
|
||||
CREATE INDEX IF NOT EXISTS routing_group_bindings_subject_idx
|
||||
ON public.routing_group_bindings USING btree (subject_type, subject_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.routing_group_versions (
|
||||
id character varying(64) NOT NULL,
|
||||
group_id character varying(64) NOT NULL,
|
||||
version bigint NOT NULL,
|
||||
config_json jsonb NOT NULL,
|
||||
created_at bigint NOT NULL,
|
||||
created_by character varying(64)
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'routing_group_versions_pkey'
|
||||
) THEN
|
||||
ALTER TABLE ONLY public.routing_group_versions
|
||||
ADD CONSTRAINT routing_group_versions_pkey PRIMARY KEY (id);
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'routing_group_versions_group_version_key'
|
||||
) THEN
|
||||
ALTER TABLE ONLY public.routing_group_versions
|
||||
ADD CONSTRAINT routing_group_versions_group_version_key UNIQUE (group_id, version);
|
||||
END IF;
|
||||
END $$;
|
||||
CREATE INDEX IF NOT EXISTS routing_group_versions_group_id_idx
|
||||
ON public.routing_group_versions USING btree (group_id);
|
||||
@@ -0,0 +1,45 @@
|
||||
CREATE TABLE IF NOT EXISTS routing_groups (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
is_system_default INTEGER NOT NULL DEFAULT 0,
|
||||
config_json TEXT NOT NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
published_at INTEGER,
|
||||
UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS routing_groups_system_default_idx
|
||||
ON routing_groups (is_system_default, enabled);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_group_bindings (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
subject_type TEXT NOT NULL,
|
||||
subject_id TEXT NOT NULL,
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
allow_explicit_select INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS routing_group_bindings_group_id_idx
|
||||
ON routing_group_bindings (group_id);
|
||||
CREATE INDEX IF NOT EXISTS routing_group_bindings_subject_idx
|
||||
ON routing_group_bindings (subject_type, subject_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_group_versions (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
config_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
created_by TEXT,
|
||||
UNIQUE (group_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS routing_group_versions_group_id_idx
|
||||
ON routing_group_versions (group_id);
|
||||
@@ -383,3 +383,45 @@ CREATE TABLE IF NOT EXISTS global_models (
|
||||
UNIQUE KEY global_models_name_key (`name`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_groups (
|
||||
`id` VARCHAR(64) NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` LONGTEXT,
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`is_system_default` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`config_json` JSON NOT NULL,
|
||||
`version` BIGINT NOT NULL DEFAULT 1,
|
||||
`created_at` BIGINT NOT NULL,
|
||||
`updated_at` BIGINT NOT NULL,
|
||||
`published_at` BIGINT,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY routing_groups_name_key (`name`),
|
||||
KEY routing_groups_system_default_idx (`is_system_default`, `enabled`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_group_bindings (
|
||||
`id` VARCHAR(64) NOT NULL,
|
||||
`group_id` VARCHAR(64) NOT NULL,
|
||||
`subject_type` VARCHAR(32) NOT NULL,
|
||||
`subject_id` VARCHAR(64) NOT NULL,
|
||||
`is_default` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`allow_explicit_select` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` BIGINT NOT NULL,
|
||||
`updated_at` BIGINT NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY routing_group_bindings_group_id_idx (`group_id`),
|
||||
KEY routing_group_bindings_subject_idx (`subject_type`, `subject_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_group_versions (
|
||||
`id` VARCHAR(64) NOT NULL,
|
||||
`group_id` VARCHAR(64) NOT NULL,
|
||||
`version` BIGINT NOT NULL,
|
||||
`config_json` JSON NOT NULL,
|
||||
`created_at` BIGINT NOT NULL,
|
||||
`created_by` VARCHAR(64),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY routing_group_versions_group_version_key (`group_id`, `version`),
|
||||
KEY routing_group_versions_group_id_idx (`group_id`)
|
||||
);
|
||||
|
||||
|
||||
@@ -396,3 +396,48 @@ CREATE TABLE IF NOT EXISTS public.global_models (
|
||||
ALTER TABLE ONLY public.global_models ADD CONSTRAINT global_models_pkey PRIMARY KEY (id);
|
||||
ALTER TABLE ONLY public.global_models ADD CONSTRAINT global_models_name_key UNIQUE (name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.routing_groups (
|
||||
id character varying(64) NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
description text,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
is_system_default boolean DEFAULT false NOT NULL,
|
||||
config_json jsonb NOT NULL,
|
||||
version bigint DEFAULT 1 NOT NULL,
|
||||
created_at bigint NOT NULL,
|
||||
updated_at bigint NOT NULL,
|
||||
published_at bigint
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY public.routing_groups ADD CONSTRAINT routing_groups_pkey PRIMARY KEY (id);
|
||||
ALTER TABLE ONLY public.routing_groups ADD CONSTRAINT routing_groups_name_key UNIQUE (name);
|
||||
CREATE INDEX IF NOT EXISTS routing_groups_system_default_idx ON public.routing_groups USING btree (is_system_default, enabled);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.routing_group_bindings (
|
||||
id character varying(64) NOT NULL,
|
||||
group_id character varying(64) NOT NULL,
|
||||
subject_type character varying(32) NOT NULL,
|
||||
subject_id character varying(64) NOT NULL,
|
||||
is_default boolean DEFAULT false NOT NULL,
|
||||
allow_explicit_select boolean DEFAULT true NOT NULL,
|
||||
created_at bigint NOT NULL,
|
||||
updated_at bigint NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY public.routing_group_bindings ADD CONSTRAINT routing_group_bindings_pkey PRIMARY KEY (id);
|
||||
CREATE INDEX IF NOT EXISTS routing_group_bindings_group_id_idx ON public.routing_group_bindings USING btree (group_id);
|
||||
CREATE INDEX IF NOT EXISTS routing_group_bindings_subject_idx ON public.routing_group_bindings USING btree (subject_type, subject_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.routing_group_versions (
|
||||
id character varying(64) NOT NULL,
|
||||
group_id character varying(64) NOT NULL,
|
||||
version bigint NOT NULL,
|
||||
config_json jsonb NOT NULL,
|
||||
created_at bigint NOT NULL,
|
||||
created_by character varying(64)
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY public.routing_group_versions ADD CONSTRAINT routing_group_versions_pkey PRIMARY KEY (id);
|
||||
ALTER TABLE ONLY public.routing_group_versions ADD CONSTRAINT routing_group_versions_group_version_key UNIQUE (group_id, version);
|
||||
CREATE INDEX IF NOT EXISTS routing_group_versions_group_id_idx ON public.routing_group_versions USING btree (group_id);
|
||||
|
||||
|
||||
@@ -370,3 +370,42 @@ CREATE TABLE IF NOT EXISTS global_models (
|
||||
UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_groups (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
is_system_default INTEGER NOT NULL DEFAULT 0,
|
||||
config_json TEXT NOT NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
published_at INTEGER,
|
||||
UNIQUE (name)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS routing_groups_system_default_idx ON routing_groups (is_system_default, enabled);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_group_bindings (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
subject_type TEXT NOT NULL,
|
||||
subject_id TEXT NOT NULL,
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
allow_explicit_select INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS routing_group_bindings_group_id_idx ON routing_group_bindings (group_id);
|
||||
CREATE INDEX IF NOT EXISTS routing_group_bindings_subject_idx ON routing_group_bindings (subject_type, subject_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_group_versions (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
config_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
created_by TEXT,
|
||||
UNIQUE (group_id, version)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS routing_group_versions_group_id_idx ON routing_group_versions (group_id);
|
||||
|
||||
|
||||
@@ -1694,3 +1694,155 @@ type = "unix_seconds"
|
||||
[[table.global_models.uniques]]
|
||||
name = "global_models_name_key"
|
||||
columns = ["name"]
|
||||
|
||||
[table.routing_groups]
|
||||
domain = "provider_catalog"
|
||||
order = 110
|
||||
primary_key = ["id"]
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "id"
|
||||
type = "text_id"
|
||||
length = 64
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "name"
|
||||
type = "text"
|
||||
length = 255
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "description"
|
||||
type = "long_text"
|
||||
nullable = true
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "enabled"
|
||||
type = "bool"
|
||||
default = true
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "is_system_default"
|
||||
type = "bool"
|
||||
default = false
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "config_json"
|
||||
type = "json"
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "version"
|
||||
type = "int64"
|
||||
default = 1
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "created_at"
|
||||
type = "unix_seconds"
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "updated_at"
|
||||
type = "unix_seconds"
|
||||
|
||||
[[table.routing_groups.columns]]
|
||||
name = "published_at"
|
||||
type = "unix_seconds"
|
||||
nullable = true
|
||||
|
||||
[[table.routing_groups.uniques]]
|
||||
name = "routing_groups_name_key"
|
||||
columns = ["name"]
|
||||
|
||||
[[table.routing_groups.indexes]]
|
||||
name = "routing_groups_system_default_idx"
|
||||
columns = ["is_system_default", "enabled"]
|
||||
|
||||
[table.routing_group_bindings]
|
||||
domain = "provider_catalog"
|
||||
order = 111
|
||||
primary_key = ["id"]
|
||||
|
||||
[[table.routing_group_bindings.columns]]
|
||||
name = "id"
|
||||
type = "text_id"
|
||||
length = 64
|
||||
|
||||
[[table.routing_group_bindings.columns]]
|
||||
name = "group_id"
|
||||
type = "text_id"
|
||||
length = 64
|
||||
|
||||
[[table.routing_group_bindings.columns]]
|
||||
name = "subject_type"
|
||||
type = "text"
|
||||
length = 32
|
||||
|
||||
[[table.routing_group_bindings.columns]]
|
||||
name = "subject_id"
|
||||
type = "text_id"
|
||||
length = 64
|
||||
|
||||
[[table.routing_group_bindings.columns]]
|
||||
name = "is_default"
|
||||
type = "bool"
|
||||
default = false
|
||||
|
||||
[[table.routing_group_bindings.columns]]
|
||||
name = "allow_explicit_select"
|
||||
type = "bool"
|
||||
default = true
|
||||
|
||||
[[table.routing_group_bindings.columns]]
|
||||
name = "created_at"
|
||||
type = "unix_seconds"
|
||||
|
||||
[[table.routing_group_bindings.columns]]
|
||||
name = "updated_at"
|
||||
type = "unix_seconds"
|
||||
|
||||
[[table.routing_group_bindings.indexes]]
|
||||
name = "routing_group_bindings_group_id_idx"
|
||||
columns = ["group_id"]
|
||||
|
||||
[[table.routing_group_bindings.indexes]]
|
||||
name = "routing_group_bindings_subject_idx"
|
||||
columns = ["subject_type", "subject_id"]
|
||||
|
||||
[table.routing_group_versions]
|
||||
domain = "provider_catalog"
|
||||
order = 112
|
||||
primary_key = ["id"]
|
||||
|
||||
[[table.routing_group_versions.columns]]
|
||||
name = "id"
|
||||
type = "text_id"
|
||||
length = 64
|
||||
|
||||
[[table.routing_group_versions.columns]]
|
||||
name = "group_id"
|
||||
type = "text_id"
|
||||
length = 64
|
||||
|
||||
[[table.routing_group_versions.columns]]
|
||||
name = "version"
|
||||
type = "int64"
|
||||
|
||||
[[table.routing_group_versions.columns]]
|
||||
name = "config_json"
|
||||
type = "json"
|
||||
|
||||
[[table.routing_group_versions.columns]]
|
||||
name = "created_at"
|
||||
type = "unix_seconds"
|
||||
|
||||
[[table.routing_group_versions.columns]]
|
||||
name = "created_by"
|
||||
type = "text_id"
|
||||
length = 64
|
||||
nullable = true
|
||||
|
||||
[[table.routing_group_versions.uniques]]
|
||||
name = "routing_group_versions_group_version_key"
|
||||
columns = ["group_id", "version"]
|
||||
|
||||
[[table.routing_group_versions.indexes]]
|
||||
name = "routing_group_versions_group_id_idx"
|
||||
columns = ["group_id"]
|
||||
|
||||
@@ -50,6 +50,9 @@ use crate::repository::proxy_nodes::{
|
||||
use crate::repository::quota::{
|
||||
MysqlProviderQuotaRepository, ProviderQuotaReadRepository, ProviderQuotaWriteRepository,
|
||||
};
|
||||
use crate::repository::routing_profiles::{
|
||||
MysqlRoutingGroupRepository, RoutingGroupReadRepository, RoutingGroupWriteRepository,
|
||||
};
|
||||
use crate::repository::settlement::{MysqlSettlementRepository, SettlementWriteRepository};
|
||||
use crate::repository::usage::{
|
||||
MysqlUsageReadRepository, MysqlUsageWriteRepository, UsageReadRepository, UsageWriteRepository,
|
||||
@@ -195,6 +198,14 @@ impl MysqlBackend {
|
||||
Arc::new(MysqlPoolMemberScoreRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
pub fn routing_group_read_repository(&self) -> Arc<dyn RoutingGroupReadRepository> {
|
||||
Arc::new(MysqlRoutingGroupRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
pub fn routing_group_write_repository(&self) -> Arc<dyn RoutingGroupWriteRepository> {
|
||||
Arc::new(MysqlRoutingGroupRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
pub fn proxy_node_read_repository(&self) -> Arc<dyn ProxyNodeReadRepository> {
|
||||
Arc::new(MysqlProxyNodeReadRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@ use crate::repository::proxy_nodes::{
|
||||
use crate::repository::quota::{
|
||||
ProviderQuotaReadRepository, ProviderQuotaWriteRepository, SqlxProviderQuotaRepository,
|
||||
};
|
||||
use crate::repository::routing_profiles::{
|
||||
PostgresRoutingGroupRepository, RoutingGroupReadRepository, RoutingGroupWriteRepository,
|
||||
};
|
||||
use crate::repository::settlement::{SettlementWriteRepository, SqlxSettlementRepository};
|
||||
use crate::repository::usage::{
|
||||
SqlxUsageReadRepository, UsageReadRepository, UsageWriteRepository,
|
||||
@@ -206,6 +209,14 @@ impl PostgresBackend {
|
||||
Arc::new(PostgresPoolMemberScoreRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
pub fn routing_group_read_repository(&self) -> Arc<dyn RoutingGroupReadRepository> {
|
||||
Arc::new(PostgresRoutingGroupRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
pub fn routing_group_write_repository(&self) -> Arc<dyn RoutingGroupWriteRepository> {
|
||||
Arc::new(PostgresRoutingGroupRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
pub fn provider_quota_read_repository(&self) -> Arc<dyn ProviderQuotaReadRepository> {
|
||||
Arc::new(SqlxProviderQuotaRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::repository::pool_scores::PoolScoreReadRepository;
|
||||
use crate::repository::provider_catalog::ProviderCatalogReadRepository;
|
||||
use crate::repository::proxy_nodes::ProxyNodeReadRepository;
|
||||
use crate::repository::quota::ProviderQuotaReadRepository;
|
||||
use crate::repository::routing_profiles::RoutingGroupReadRepository;
|
||||
use crate::repository::usage::UsageReadRepository;
|
||||
use crate::repository::users::UserReadRepository;
|
||||
use crate::repository::video_tasks::VideoTaskReadRepository;
|
||||
@@ -41,6 +42,7 @@ pub struct DataReadRepositories {
|
||||
request_candidates: Option<Arc<dyn RequestCandidateReadRepository>>,
|
||||
provider_catalog: Option<Arc<dyn ProviderCatalogReadRepository>>,
|
||||
provider_quotas: Option<Arc<dyn ProviderQuotaReadRepository>>,
|
||||
routing_groups: Option<Arc<dyn RoutingGroupReadRepository>>,
|
||||
usage: Option<Arc<dyn UsageReadRepository>>,
|
||||
users: Option<Arc<dyn UserReadRepository>>,
|
||||
video_tasks: Option<Arc<dyn VideoTaskReadRepository>>,
|
||||
@@ -72,6 +74,7 @@ impl fmt::Debug for DataReadRepositories {
|
||||
.field("has_request_candidates", &self.request_candidates.is_some())
|
||||
.field("has_provider_catalog", &self.provider_catalog.is_some())
|
||||
.field("has_provider_quotas", &self.provider_quotas.is_some())
|
||||
.field("has_routing_groups", &self.routing_groups.is_some())
|
||||
.field("has_usage", &self.usage.is_some())
|
||||
.field("has_users", &self.users.is_some())
|
||||
.field("has_video_tasks", &self.video_tasks.is_some())
|
||||
@@ -151,6 +154,10 @@ impl DataReadRepositories {
|
||||
.map(PostgresBackend::provider_quota_read_repository)
|
||||
.or_else(|| mysql.map(MysqlBackend::provider_quota_read_repository))
|
||||
.or_else(|| sqlite.map(SqliteBackend::provider_quota_read_repository)),
|
||||
routing_groups: postgres
|
||||
.map(PostgresBackend::routing_group_read_repository)
|
||||
.or_else(|| mysql.map(MysqlBackend::routing_group_read_repository))
|
||||
.or_else(|| sqlite.map(SqliteBackend::routing_group_read_repository)),
|
||||
usage: postgres
|
||||
.map(PostgresBackend::usage_read_repository)
|
||||
.or_else(|| mysql.map(MysqlBackend::usage_read_repository))
|
||||
@@ -241,6 +248,10 @@ impl DataReadRepositories {
|
||||
self.provider_quotas.clone()
|
||||
}
|
||||
|
||||
pub fn routing_groups(&self) -> Option<Arc<dyn RoutingGroupReadRepository>> {
|
||||
self.routing_groups.clone()
|
||||
}
|
||||
|
||||
pub fn usage(&self) -> Option<Arc<dyn UsageReadRepository>> {
|
||||
self.usage.clone()
|
||||
}
|
||||
@@ -274,6 +285,7 @@ impl DataReadRepositories {
|
||||
|| self.request_candidates.is_some()
|
||||
|| self.provider_catalog.is_some()
|
||||
|| self.provider_quotas.is_some()
|
||||
|| self.routing_groups.is_some()
|
||||
|| self.usage.is_some()
|
||||
|| self.users.is_some()
|
||||
|| self.video_tasks.is_some()
|
||||
|
||||
@@ -50,6 +50,9 @@ use crate::repository::proxy_nodes::{
|
||||
use crate::repository::quota::{
|
||||
ProviderQuotaReadRepository, ProviderQuotaWriteRepository, SqliteProviderQuotaRepository,
|
||||
};
|
||||
use crate::repository::routing_profiles::{
|
||||
RoutingGroupReadRepository, RoutingGroupWriteRepository, SqliteRoutingGroupRepository,
|
||||
};
|
||||
use crate::repository::settlement::{SettlementWriteRepository, SqliteSettlementRepository};
|
||||
use crate::repository::usage::{
|
||||
SqliteUsageReadRepository, SqliteUsageWriteRepository, UsageReadRepository,
|
||||
@@ -208,6 +211,14 @@ impl SqliteBackend {
|
||||
Arc::new(SqlitePoolMemberScoreRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
pub fn routing_group_read_repository(&self) -> Arc<dyn RoutingGroupReadRepository> {
|
||||
Arc::new(SqliteRoutingGroupRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
pub fn routing_group_write_repository(&self) -> Arc<dyn RoutingGroupWriteRepository> {
|
||||
Arc::new(SqliteRoutingGroupRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
pub fn proxy_node_read_repository(&self) -> Arc<dyn ProxyNodeReadRepository> {
|
||||
Arc::new(SqliteProxyNodeReadRepository::new(self.pool_clone()))
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::repository::pool_scores::PoolMemberScoreWriteRepository;
|
||||
use crate::repository::provider_catalog::ProviderCatalogWriteRepository;
|
||||
use crate::repository::proxy_nodes::ProxyNodeWriteRepository;
|
||||
use crate::repository::quota::ProviderQuotaWriteRepository;
|
||||
use crate::repository::routing_profiles::RoutingGroupWriteRepository;
|
||||
use crate::repository::settlement::SettlementWriteRepository;
|
||||
use crate::repository::usage::UsageWriteRepository;
|
||||
use crate::repository::video_tasks::VideoTaskWriteRepository;
|
||||
@@ -35,6 +36,7 @@ pub struct DataWriteRepositories {
|
||||
proxy_nodes: Option<Arc<dyn ProxyNodeWriteRepository>>,
|
||||
provider_catalog: Option<Arc<dyn ProviderCatalogWriteRepository>>,
|
||||
provider_quotas: Option<Arc<dyn ProviderQuotaWriteRepository>>,
|
||||
routing_groups: Option<Arc<dyn RoutingGroupWriteRepository>>,
|
||||
settlement: Option<Arc<dyn SettlementWriteRepository>>,
|
||||
usage: Option<Arc<dyn UsageWriteRepository>>,
|
||||
video_tasks: Option<Arc<dyn VideoTaskWriteRepository>>,
|
||||
@@ -60,6 +62,7 @@ impl fmt::Debug for DataWriteRepositories {
|
||||
.field("has_proxy_nodes", &self.proxy_nodes.is_some())
|
||||
.field("has_provider_catalog", &self.provider_catalog.is_some())
|
||||
.field("has_provider_quotas", &self.provider_quotas.is_some())
|
||||
.field("has_routing_groups", &self.routing_groups.is_some())
|
||||
.field("has_settlement", &self.settlement.is_some())
|
||||
.field("has_usage", &self.usage.is_some())
|
||||
.field("has_video_tasks", &self.video_tasks.is_some())
|
||||
@@ -127,6 +130,10 @@ impl DataWriteRepositories {
|
||||
.map(PostgresBackend::provider_quota_write_repository)
|
||||
.or_else(|| mysql.map(MysqlBackend::provider_quota_write_repository))
|
||||
.or_else(|| sqlite.map(SqliteBackend::provider_quota_write_repository)),
|
||||
routing_groups: postgres
|
||||
.map(PostgresBackend::routing_group_write_repository)
|
||||
.or_else(|| mysql.map(MysqlBackend::routing_group_write_repository))
|
||||
.or_else(|| sqlite.map(SqliteBackend::routing_group_write_repository)),
|
||||
settlement: postgres
|
||||
.map(PostgresBackend::settlement_write_repository)
|
||||
.or_else(|| mysql.map(MysqlBackend::settlement_write_repository))
|
||||
@@ -203,6 +210,10 @@ impl DataWriteRepositories {
|
||||
self.provider_quotas.clone()
|
||||
}
|
||||
|
||||
pub fn routing_groups(&self) -> Option<Arc<dyn RoutingGroupWriteRepository>> {
|
||||
self.routing_groups.clone()
|
||||
}
|
||||
|
||||
pub fn provider_catalog(&self) -> Option<Arc<dyn ProviderCatalogWriteRepository>> {
|
||||
self.provider_catalog.clone()
|
||||
}
|
||||
@@ -233,6 +244,7 @@ impl DataWriteRepositories {
|
||||
|| self.proxy_nodes.is_some()
|
||||
|| self.provider_catalog.is_some()
|
||||
|| self.provider_quotas.is_some()
|
||||
|| self.routing_groups.is_some()
|
||||
|| self.settlement.is_some()
|
||||
|| self.usage.is_some()
|
||||
|| self.video_tasks.is_some()
|
||||
|
||||
@@ -7,7 +7,7 @@ use tracing::info;
|
||||
// Generated by build.rs from schema/bootstrap/postgres.
|
||||
pub(crate) static EMPTY_DATABASE_SNAPSHOT_SQL: &str =
|
||||
include_str!(concat!(env!("OUT_DIR"), "/empty_database_snapshot.sql"));
|
||||
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260515000000;
|
||||
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260516000000;
|
||||
|
||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||
SELECT COUNT(*)::BIGINT
|
||||
@@ -29,6 +29,7 @@ WHERE table_schema = 'public'
|
||||
'oauth_providers',
|
||||
'provider_api_keys',
|
||||
'proxy_nodes',
|
||||
'routing_groups',
|
||||
'user_groups',
|
||||
'usage_routing_snapshots',
|
||||
'usage_settlement_snapshots'
|
||||
|
||||
@@ -305,6 +305,7 @@ fn empty_database_snapshot_covers_current_cutoff_versions() {
|
||||
20260512090000,
|
||||
20260512110000,
|
||||
20260515000000,
|
||||
20260516000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -587,6 +588,7 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
|
||||
20260512000000,
|
||||
20260512090000,
|
||||
20260512110000,
|
||||
20260516000000,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -603,6 +605,7 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
|
||||
20260512000000,
|
||||
20260512090000,
|
||||
20260512110000,
|
||||
20260516000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -1119,6 +1122,7 @@ fn pending_migrations_from_applied_skips_versions_already_applied() {
|
||||
20260512090000,
|
||||
20260512110000,
|
||||
20260515000000,
|
||||
20260516000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ pub mod provider_catalog;
|
||||
pub mod provider_oauth;
|
||||
pub mod proxy_nodes;
|
||||
pub mod quota;
|
||||
pub mod routing_profiles;
|
||||
pub mod settlement;
|
||||
pub mod system;
|
||||
pub mod usage;
|
||||
|
||||
342
crates/aether-data/src/repository/routing_profiles/memory.rs
Normal file
342
crates/aether-data/src/repository/routing_profiles/memory.rs
Normal file
@@ -0,0 +1,342 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::{
|
||||
CreateRoutingGroupBindingRecord, CreateRoutingGroupRecord, CreateRoutingGroupVersionRecord,
|
||||
RoutingGroupBindingQuery, RoutingGroupLookupKey, RoutingGroupReadRepository,
|
||||
RoutingGroupWriteRepository, StoredRoutingGroup, StoredRoutingGroupBinding,
|
||||
StoredRoutingGroupVersion, UpdateRoutingGroupBindingRecord, UpdateRoutingGroupRecord,
|
||||
};
|
||||
use crate::DataLayerError;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InMemoryRoutingGroupRepository {
|
||||
groups: RwLock<BTreeMap<String, StoredRoutingGroup>>,
|
||||
bindings: RwLock<BTreeMap<String, StoredRoutingGroupBinding>>,
|
||||
versions: RwLock<BTreeMap<String, StoredRoutingGroupVersion>>,
|
||||
}
|
||||
|
||||
impl InMemoryRoutingGroupRepository {
|
||||
pub fn seed<I, B, V>(groups: I, bindings: B, versions: V) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = StoredRoutingGroup>,
|
||||
B: IntoIterator<Item = StoredRoutingGroupBinding>,
|
||||
V: IntoIterator<Item = StoredRoutingGroupVersion>,
|
||||
{
|
||||
Self {
|
||||
groups: RwLock::new(
|
||||
groups
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect(),
|
||||
),
|
||||
bindings: RwLock::new(
|
||||
bindings
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect(),
|
||||
),
|
||||
versions: RwLock::new(
|
||||
versions
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RoutingGroupReadRepository for InMemoryRoutingGroupRepository {
|
||||
async fn list_routing_groups(&self) -> Result<Vec<StoredRoutingGroup>, DataLayerError> {
|
||||
let mut groups = self
|
||||
.groups
|
||||
.read()
|
||||
.expect("routing group repository lock")
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
groups.sort_by(|left, right| left.name.cmp(&right.name).then(left.id.cmp(&right.id)));
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
async fn find_routing_group(
|
||||
&self,
|
||||
lookup: RoutingGroupLookupKey<'_>,
|
||||
) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
let groups = self.groups.read().expect("routing group repository lock");
|
||||
Ok(match lookup {
|
||||
RoutingGroupLookupKey::Id(id) => groups.get(id).cloned(),
|
||||
RoutingGroupLookupKey::Name(name) => {
|
||||
groups.values().find(|group| group.name == name).cloned()
|
||||
}
|
||||
RoutingGroupLookupKey::SystemDefault => groups
|
||||
.values()
|
||||
.find(|group| group.is_system_default && group.enabled)
|
||||
.cloned(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_routing_group_bindings(
|
||||
&self,
|
||||
query: &RoutingGroupBindingQuery,
|
||||
) -> Result<Vec<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let mut rows = self
|
||||
.bindings
|
||||
.read()
|
||||
.expect("routing group binding repository lock")
|
||||
.values()
|
||||
.filter(|row| {
|
||||
query
|
||||
.group_id
|
||||
.as_ref()
|
||||
.is_none_or(|group_id| &row.group_id == group_id)
|
||||
&& query
|
||||
.subject_type
|
||||
.as_ref()
|
||||
.is_none_or(|subject_type| &row.subject_type == subject_type)
|
||||
&& query
|
||||
.subject_id
|
||||
.as_ref()
|
||||
.is_none_or(|subject_id| &row.subject_id == subject_id)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
rows.sort_by(|left, right| {
|
||||
left.created_at
|
||||
.cmp(&right.created_at)
|
||||
.then(left.id.cmp(&right.id))
|
||||
});
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
async fn list_routing_group_versions(
|
||||
&self,
|
||||
group_id: &str,
|
||||
) -> Result<Vec<StoredRoutingGroupVersion>, DataLayerError> {
|
||||
let mut rows = self
|
||||
.versions
|
||||
.read()
|
||||
.expect("routing group version repository lock")
|
||||
.values()
|
||||
.filter(|row| row.group_id == group_id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
rows.sort_by(|left, right| {
|
||||
right
|
||||
.version
|
||||
.cmp(&left.version)
|
||||
.then(right.created_at.cmp(&left.created_at))
|
||||
});
|
||||
Ok(rows)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RoutingGroupWriteRepository for InMemoryRoutingGroupRepository {
|
||||
async fn create_routing_group(
|
||||
&self,
|
||||
record: CreateRoutingGroupRecord,
|
||||
) -> Result<StoredRoutingGroup, DataLayerError> {
|
||||
let group = StoredRoutingGroup::new(record)?;
|
||||
self.groups
|
||||
.write()
|
||||
.expect("routing group repository lock")
|
||||
.insert(group.id.clone(), group.clone());
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_routing_group(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: UpdateRoutingGroupRecord,
|
||||
) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
let mut groups = self.groups.write().expect("routing group repository lock");
|
||||
let Some(group) = groups.get_mut(id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(name) = patch.name {
|
||||
if name.trim().is_empty() {
|
||||
return Err(DataLayerError::InvalidInput(
|
||||
"routing_groups.name is empty".to_string(),
|
||||
));
|
||||
}
|
||||
group.name = name;
|
||||
}
|
||||
if let Some(description) = patch.description {
|
||||
group.description = description;
|
||||
}
|
||||
if let Some(enabled) = patch.enabled {
|
||||
group.enabled = enabled;
|
||||
}
|
||||
if let Some(is_system_default) = patch.is_system_default {
|
||||
group.is_system_default = is_system_default;
|
||||
}
|
||||
if let Some(config_json) = patch.config_json {
|
||||
if !config_json.is_object() {
|
||||
return Err(DataLayerError::InvalidInput(
|
||||
"routing_groups.config_json must be a JSON object".to_string(),
|
||||
));
|
||||
}
|
||||
group.config_json = config_json;
|
||||
}
|
||||
if let Some(version) = patch.version {
|
||||
group.version = version.max(1);
|
||||
}
|
||||
if let Some(published_at) = patch.published_at {
|
||||
group.published_at = published_at;
|
||||
}
|
||||
group.updated_at = patch.updated_at;
|
||||
Ok(Some(group.clone()))
|
||||
}
|
||||
|
||||
async fn delete_routing_group(&self, id: &str) -> Result<bool, DataLayerError> {
|
||||
Ok(self
|
||||
.groups
|
||||
.write()
|
||||
.expect("routing group repository lock")
|
||||
.remove(id)
|
||||
.is_some())
|
||||
}
|
||||
|
||||
async fn create_routing_group_binding(
|
||||
&self,
|
||||
record: CreateRoutingGroupBindingRecord,
|
||||
) -> Result<StoredRoutingGroupBinding, DataLayerError> {
|
||||
let binding = StoredRoutingGroupBinding::new(record)?;
|
||||
self.bindings
|
||||
.write()
|
||||
.expect("routing group binding repository lock")
|
||||
.insert(binding.id.clone(), binding.clone());
|
||||
Ok(binding)
|
||||
}
|
||||
|
||||
async fn delete_routing_group_binding(&self, id: &str) -> Result<bool, DataLayerError> {
|
||||
Ok(self
|
||||
.bindings
|
||||
.write()
|
||||
.expect("routing group binding repository lock")
|
||||
.remove(id)
|
||||
.is_some())
|
||||
}
|
||||
|
||||
async fn update_routing_group_binding(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: UpdateRoutingGroupBindingRecord,
|
||||
) -> Result<Option<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let mut bindings = self
|
||||
.bindings
|
||||
.write()
|
||||
.expect("routing group binding repository lock");
|
||||
let Some(binding) = bindings.get_mut(id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(group_id) = patch.group_id {
|
||||
if group_id.trim().is_empty() {
|
||||
return Err(DataLayerError::InvalidInput(
|
||||
"routing_group_bindings.group_id is empty".to_string(),
|
||||
));
|
||||
}
|
||||
binding.group_id = group_id;
|
||||
}
|
||||
if let Some(subject_type) = patch.subject_type {
|
||||
binding.subject_type = subject_type;
|
||||
}
|
||||
if let Some(subject_id) = patch.subject_id {
|
||||
if subject_id.trim().is_empty() {
|
||||
return Err(DataLayerError::InvalidInput(
|
||||
"routing_group_bindings.subject_id is empty".to_string(),
|
||||
));
|
||||
}
|
||||
binding.subject_id = subject_id;
|
||||
}
|
||||
if let Some(is_default) = patch.is_default {
|
||||
binding.is_default = is_default;
|
||||
}
|
||||
if let Some(allow_explicit_select) = patch.allow_explicit_select {
|
||||
binding.allow_explicit_select = allow_explicit_select;
|
||||
}
|
||||
binding.updated_at = patch.updated_at;
|
||||
Ok(Some(binding.clone()))
|
||||
}
|
||||
|
||||
async fn create_routing_group_version(
|
||||
&self,
|
||||
record: CreateRoutingGroupVersionRecord,
|
||||
) -> Result<StoredRoutingGroupVersion, DataLayerError> {
|
||||
let version = StoredRoutingGroupVersion::new(record)?;
|
||||
self.versions
|
||||
.write()
|
||||
.expect("routing group version repository lock")
|
||||
.insert(version.id.clone(), version.clone());
|
||||
Ok(version)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use aether_data_contracts::repository::routing_profiles::RoutingGroupBindingSubject;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn stores_groups_bindings_and_versions() {
|
||||
let repository = InMemoryRoutingGroupRepository::default();
|
||||
let group = repository
|
||||
.create_routing_group(CreateRoutingGroupRecord {
|
||||
id: "group-1".to_string(),
|
||||
name: "default".to_string(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
is_system_default: true,
|
||||
config_json: json!({}),
|
||||
version: 1,
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
published_at: None,
|
||||
})
|
||||
.await
|
||||
.expect("group should store");
|
||||
|
||||
assert_eq!(
|
||||
repository
|
||||
.find_routing_group(RoutingGroupLookupKey::SystemDefault)
|
||||
.await
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|group| group.id.as_str()),
|
||||
Some(group.id.as_str())
|
||||
);
|
||||
|
||||
repository
|
||||
.create_routing_group_binding(CreateRoutingGroupBindingRecord {
|
||||
id: "binding-1".to_string(),
|
||||
group_id: "group-1".to_string(),
|
||||
subject_type: RoutingGroupBindingSubject::ApiKey,
|
||||
subject_id: "api-key-1".to_string(),
|
||||
is_default: true,
|
||||
allow_explicit_select: true,
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
repository
|
||||
.list_routing_group_bindings(&RoutingGroupBindingQuery {
|
||||
subject_type: Some(RoutingGroupBindingSubject::ApiKey),
|
||||
subject_id: Some("api-key-1".to_string()),
|
||||
group_id: None,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
16
crates/aether-data/src/repository/routing_profiles/mod.rs
Normal file
16
crates/aether-data/src/repository/routing_profiles/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
mod memory;
|
||||
mod mysql;
|
||||
mod postgres;
|
||||
mod sqlite;
|
||||
|
||||
pub(crate) use aether_data_contracts::repository::routing_profiles::{
|
||||
CreateRoutingGroupBindingRecord, CreateRoutingGroupRecord, CreateRoutingGroupVersionRecord,
|
||||
RoutingGroupBindingQuery, RoutingGroupBindingSubject, RoutingGroupLookupKey,
|
||||
RoutingGroupReadRepository, RoutingGroupWriteRepository, StoredRoutingGroup,
|
||||
StoredRoutingGroupBinding, StoredRoutingGroupVersion, UpdateRoutingGroupBindingRecord,
|
||||
UpdateRoutingGroupRecord,
|
||||
};
|
||||
pub use memory::InMemoryRoutingGroupRepository;
|
||||
pub use mysql::MysqlRoutingGroupRepository;
|
||||
pub use postgres::PostgresRoutingGroupRepository;
|
||||
pub use sqlite::SqliteRoutingGroupRepository;
|
||||
417
crates/aether-data/src/repository/routing_profiles/mysql.rs
Normal file
417
crates/aether-data/src/repository/routing_profiles/mysql.rs
Normal file
@@ -0,0 +1,417 @@
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use sqlx::{mysql::MySqlRow, Row};
|
||||
|
||||
use super::postgres::{
|
||||
apply_binding_patch, apply_group_patch, binding_subject_from_database,
|
||||
binding_subject_to_database,
|
||||
};
|
||||
use super::*;
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
|
||||
const ROUTING_GROUP_SELECT: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
is_system_default,
|
||||
config_json,
|
||||
version,
|
||||
created_at,
|
||||
updated_at,
|
||||
published_at
|
||||
FROM routing_groups
|
||||
"#;
|
||||
|
||||
const ROUTING_GROUP_BINDING_SELECT: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
group_id,
|
||||
subject_type,
|
||||
subject_id,
|
||||
is_default,
|
||||
allow_explicit_select,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM routing_group_bindings
|
||||
"#;
|
||||
|
||||
const ROUTING_GROUP_VERSION_SELECT: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
group_id,
|
||||
version,
|
||||
config_json,
|
||||
created_at,
|
||||
created_by
|
||||
FROM routing_group_versions
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MysqlRoutingGroupRepository {
|
||||
pool: MysqlPool,
|
||||
}
|
||||
|
||||
impl MysqlRoutingGroupRepository {
|
||||
pub fn new(pool: MysqlPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
async fn reload_group(&self, id: &str) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
self.find_routing_group(RoutingGroupLookupKey::Id(id)).await
|
||||
}
|
||||
|
||||
async fn find_binding_by_id(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Option<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let row = sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_BINDING_SELECT} WHERE id = ? LIMIT 1"
|
||||
))
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
row.as_ref().map(map_binding_row).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RoutingGroupReadRepository for MysqlRoutingGroupRepository {
|
||||
async fn list_routing_groups(&self) -> Result<Vec<StoredRoutingGroup>, DataLayerError> {
|
||||
let rows = sqlx::query(&format!("{ROUTING_GROUP_SELECT} ORDER BY name ASC, id ASC"))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_group_row).collect()
|
||||
}
|
||||
|
||||
async fn find_routing_group(
|
||||
&self,
|
||||
lookup: RoutingGroupLookupKey<'_>,
|
||||
) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
let row = match lookup {
|
||||
RoutingGroupLookupKey::Id(id) => sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_SELECT} WHERE id = ? LIMIT 1"
|
||||
))
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?,
|
||||
RoutingGroupLookupKey::Name(name) => sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_SELECT} WHERE name = ? LIMIT 1"
|
||||
))
|
||||
.bind(name)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?,
|
||||
RoutingGroupLookupKey::SystemDefault => sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_SELECT} WHERE is_system_default = 1 AND enabled = 1 ORDER BY updated_at DESC, id ASC LIMIT 1"
|
||||
))
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?,
|
||||
};
|
||||
row.as_ref().map(map_group_row).transpose()
|
||||
}
|
||||
|
||||
async fn list_routing_group_bindings(
|
||||
&self,
|
||||
query: &RoutingGroupBindingQuery,
|
||||
) -> Result<Vec<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let rows = sqlx::query(&format!(
|
||||
r#"
|
||||
{ROUTING_GROUP_BINDING_SELECT}
|
||||
WHERE (? IS NULL OR group_id = ?)
|
||||
AND (? IS NULL OR subject_type = ?)
|
||||
AND (? IS NULL OR subject_id = ?)
|
||||
ORDER BY created_at ASC, id ASC
|
||||
"#
|
||||
))
|
||||
.bind(query.group_id.as_deref())
|
||||
.bind(query.group_id.as_deref())
|
||||
.bind(query.subject_type.map(binding_subject_to_database))
|
||||
.bind(query.subject_type.map(binding_subject_to_database))
|
||||
.bind(query.subject_id.as_deref())
|
||||
.bind(query.subject_id.as_deref())
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_binding_row).collect()
|
||||
}
|
||||
|
||||
async fn list_routing_group_versions(
|
||||
&self,
|
||||
group_id: &str,
|
||||
) -> Result<Vec<StoredRoutingGroupVersion>, DataLayerError> {
|
||||
let rows = sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_VERSION_SELECT} WHERE group_id = ? ORDER BY version DESC, created_at DESC, id ASC"
|
||||
))
|
||||
.bind(group_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_version_row).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RoutingGroupWriteRepository for MysqlRoutingGroupRepository {
|
||||
async fn create_routing_group(
|
||||
&self,
|
||||
record: CreateRoutingGroupRecord,
|
||||
) -> Result<StoredRoutingGroup, DataLayerError> {
|
||||
let group = StoredRoutingGroup::new(record)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO routing_groups (
|
||||
id, name, description, enabled, is_system_default, config_json,
|
||||
version, created_at, updated_at, published_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&group.id)
|
||||
.bind(&group.name)
|
||||
.bind(&group.description)
|
||||
.bind(group.enabled)
|
||||
.bind(group.is_system_default)
|
||||
.bind(json_to_string(
|
||||
&group.config_json,
|
||||
"routing_groups.config_json",
|
||||
)?)
|
||||
.bind(group.version)
|
||||
.bind(group.created_at)
|
||||
.bind(group.updated_at)
|
||||
.bind(group.published_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_routing_group(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: UpdateRoutingGroupRecord,
|
||||
) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
let Some(mut group) = self.reload_group(id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
apply_group_patch(&mut group, patch)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE routing_groups
|
||||
SET name = ?,
|
||||
description = ?,
|
||||
enabled = ?,
|
||||
is_system_default = ?,
|
||||
config_json = ?,
|
||||
version = ?,
|
||||
updated_at = ?,
|
||||
published_at = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(&group.name)
|
||||
.bind(&group.description)
|
||||
.bind(group.enabled)
|
||||
.bind(group.is_system_default)
|
||||
.bind(json_to_string(
|
||||
&group.config_json,
|
||||
"routing_groups.config_json",
|
||||
)?)
|
||||
.bind(group.version)
|
||||
.bind(group.updated_at)
|
||||
.bind(group.published_at)
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(Some(group))
|
||||
}
|
||||
|
||||
async fn delete_routing_group(&self, id: &str) -> Result<bool, DataLayerError> {
|
||||
let mut tx = self.pool.begin().await.map_sql_err()?;
|
||||
sqlx::query("DELETE FROM routing_group_bindings WHERE group_id = ?")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
sqlx::query("DELETE FROM routing_group_versions WHERE group_id = ?")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
let rows_affected = sqlx::query("DELETE FROM routing_groups WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?
|
||||
.rows_affected();
|
||||
tx.commit().await.map_sql_err()?;
|
||||
Ok(rows_affected > 0)
|
||||
}
|
||||
|
||||
async fn create_routing_group_binding(
|
||||
&self,
|
||||
record: CreateRoutingGroupBindingRecord,
|
||||
) -> Result<StoredRoutingGroupBinding, DataLayerError> {
|
||||
let binding = StoredRoutingGroupBinding::new(record)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO routing_group_bindings (
|
||||
id, group_id, subject_type, subject_id, is_default,
|
||||
allow_explicit_select, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&binding.id)
|
||||
.bind(&binding.group_id)
|
||||
.bind(binding_subject_to_database(binding.subject_type))
|
||||
.bind(&binding.subject_id)
|
||||
.bind(binding.is_default)
|
||||
.bind(binding.allow_explicit_select)
|
||||
.bind(binding.created_at)
|
||||
.bind(binding.updated_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(binding)
|
||||
}
|
||||
|
||||
async fn delete_routing_group_binding(&self, id: &str) -> Result<bool, DataLayerError> {
|
||||
Ok(
|
||||
sqlx::query("DELETE FROM routing_group_bindings WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?
|
||||
.rows_affected()
|
||||
> 0,
|
||||
)
|
||||
}
|
||||
|
||||
async fn update_routing_group_binding(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: UpdateRoutingGroupBindingRecord,
|
||||
) -> Result<Option<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let Some(mut binding) = self.find_binding_by_id(id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
apply_binding_patch(&mut binding, patch)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE routing_group_bindings
|
||||
SET group_id = ?,
|
||||
subject_type = ?,
|
||||
subject_id = ?,
|
||||
is_default = ?,
|
||||
allow_explicit_select = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(&binding.group_id)
|
||||
.bind(binding_subject_to_database(binding.subject_type))
|
||||
.bind(&binding.subject_id)
|
||||
.bind(binding.is_default)
|
||||
.bind(binding.allow_explicit_select)
|
||||
.bind(binding.updated_at)
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(Some(binding))
|
||||
}
|
||||
|
||||
async fn create_routing_group_version(
|
||||
&self,
|
||||
record: CreateRoutingGroupVersionRecord,
|
||||
) -> Result<StoredRoutingGroupVersion, DataLayerError> {
|
||||
let version = StoredRoutingGroupVersion::new(record)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO routing_group_versions (
|
||||
id, group_id, version, config_json, created_at, created_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&version.id)
|
||||
.bind(&version.group_id)
|
||||
.bind(version.version)
|
||||
.bind(json_to_string(
|
||||
&version.config_json,
|
||||
"routing_group_versions.config_json",
|
||||
)?)
|
||||
.bind(version.created_at)
|
||||
.bind(&version.created_by)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(version)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_group_row(row: &MySqlRow) -> Result<StoredRoutingGroup, DataLayerError> {
|
||||
Ok(StoredRoutingGroup {
|
||||
id: row.try_get("id").map_sql_err()?,
|
||||
name: row.try_get("name").map_sql_err()?,
|
||||
description: row.try_get("description").map_sql_err()?,
|
||||
enabled: row.try_get("enabled").map_sql_err()?,
|
||||
is_system_default: row.try_get("is_system_default").map_sql_err()?,
|
||||
config_json: json_from_string(
|
||||
row.try_get("config_json").map_sql_err()?,
|
||||
"routing_groups.config_json",
|
||||
)?,
|
||||
version: row.try_get("version").map_sql_err()?,
|
||||
created_at: row.try_get("created_at").map_sql_err()?,
|
||||
updated_at: row.try_get("updated_at").map_sql_err()?,
|
||||
published_at: row.try_get("published_at").map_sql_err()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_binding_row(row: &MySqlRow) -> Result<StoredRoutingGroupBinding, DataLayerError> {
|
||||
Ok(StoredRoutingGroupBinding {
|
||||
id: row.try_get("id").map_sql_err()?,
|
||||
group_id: row.try_get("group_id").map_sql_err()?,
|
||||
subject_type: binding_subject_from_database(row.try_get("subject_type").map_sql_err()?)?,
|
||||
subject_id: row.try_get("subject_id").map_sql_err()?,
|
||||
is_default: row.try_get("is_default").map_sql_err()?,
|
||||
allow_explicit_select: row.try_get("allow_explicit_select").map_sql_err()?,
|
||||
created_at: row.try_get("created_at").map_sql_err()?,
|
||||
updated_at: row.try_get("updated_at").map_sql_err()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_version_row(row: &MySqlRow) -> Result<StoredRoutingGroupVersion, DataLayerError> {
|
||||
Ok(StoredRoutingGroupVersion {
|
||||
id: row.try_get("id").map_sql_err()?,
|
||||
group_id: row.try_get("group_id").map_sql_err()?,
|
||||
version: row.try_get("version").map_sql_err()?,
|
||||
config_json: json_from_string(
|
||||
row.try_get("config_json").map_sql_err()?,
|
||||
"routing_group_versions.config_json",
|
||||
)?,
|
||||
created_at: row.try_get("created_at").map_sql_err()?,
|
||||
created_by: row.try_get("created_by").map_sql_err()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn json_to_string(value: &Value, field_name: &str) -> Result<String, DataLayerError> {
|
||||
serde_json::to_string(value).map_err(|err| {
|
||||
DataLayerError::UnexpectedValue(format!("{field_name} contains unserializable JSON: {err}"))
|
||||
})
|
||||
}
|
||||
|
||||
fn json_from_string(value: String, field_name: &str) -> Result<Value, DataLayerError> {
|
||||
serde_json::from_str(&value).map_err(|err| {
|
||||
DataLayerError::UnexpectedValue(format!("{field_name} contains invalid JSON: {err}"))
|
||||
})
|
||||
}
|
||||
483
crates/aether-data/src/repository/routing_profiles/postgres.rs
Normal file
483
crates/aether-data/src/repository/routing_profiles/postgres.rs
Normal file
@@ -0,0 +1,483 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
|
||||
use super::*;
|
||||
use crate::error::SqlxResultExt;
|
||||
use crate::DataLayerError;
|
||||
|
||||
const ROUTING_GROUP_SELECT: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
is_system_default,
|
||||
config_json,
|
||||
version,
|
||||
created_at,
|
||||
updated_at,
|
||||
published_at
|
||||
FROM routing_groups
|
||||
"#;
|
||||
|
||||
const ROUTING_GROUP_BINDING_SELECT: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
group_id,
|
||||
subject_type,
|
||||
subject_id,
|
||||
is_default,
|
||||
allow_explicit_select,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM routing_group_bindings
|
||||
"#;
|
||||
|
||||
const ROUTING_GROUP_VERSION_SELECT: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
group_id,
|
||||
version,
|
||||
config_json,
|
||||
created_at,
|
||||
created_by
|
||||
FROM routing_group_versions
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostgresRoutingGroupRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl PostgresRoutingGroupRepository {
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
async fn reload_group(&self, id: &str) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
self.find_routing_group(RoutingGroupLookupKey::Id(id)).await
|
||||
}
|
||||
|
||||
async fn find_binding_by_id(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Option<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let row = sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_BINDING_SELECT} WHERE id = $1 LIMIT 1"
|
||||
))
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_binding_row).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RoutingGroupReadRepository for PostgresRoutingGroupRepository {
|
||||
async fn list_routing_groups(&self) -> Result<Vec<StoredRoutingGroup>, DataLayerError> {
|
||||
let sql = format!("{ROUTING_GROUP_SELECT} ORDER BY name ASC, id ASC");
|
||||
let mut rows = sqlx::query(&sql).fetch(&self.pool);
|
||||
let mut groups = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
groups.push(map_group_row(&row)?);
|
||||
}
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
async fn find_routing_group(
|
||||
&self,
|
||||
lookup: RoutingGroupLookupKey<'_>,
|
||||
) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
let row = match lookup {
|
||||
RoutingGroupLookupKey::Id(id) => sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_SELECT} WHERE id = $1 LIMIT 1"
|
||||
))
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
RoutingGroupLookupKey::Name(name) => sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_SELECT} WHERE name = $1 LIMIT 1"
|
||||
))
|
||||
.bind(name)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
RoutingGroupLookupKey::SystemDefault => sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_SELECT} WHERE is_system_default = TRUE AND enabled = TRUE ORDER BY updated_at DESC, id ASC LIMIT 1"
|
||||
))
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
};
|
||||
row.as_ref().map(map_group_row).transpose()
|
||||
}
|
||||
|
||||
async fn list_routing_group_bindings(
|
||||
&self,
|
||||
query: &RoutingGroupBindingQuery,
|
||||
) -> Result<Vec<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let sql = format!(
|
||||
r#"
|
||||
{ROUTING_GROUP_BINDING_SELECT}
|
||||
WHERE ($1::text IS NULL OR group_id = $1)
|
||||
AND ($2::text IS NULL OR subject_type = $2)
|
||||
AND ($3::text IS NULL OR subject_id = $3)
|
||||
ORDER BY created_at ASC, id ASC
|
||||
"#
|
||||
);
|
||||
let mut rows = sqlx::query(&sql)
|
||||
.bind(query.group_id.as_deref())
|
||||
.bind(query.subject_type.map(binding_subject_to_database))
|
||||
.bind(query.subject_id.as_deref())
|
||||
.fetch(&self.pool);
|
||||
let mut bindings = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
bindings.push(map_binding_row(&row)?);
|
||||
}
|
||||
Ok(bindings)
|
||||
}
|
||||
|
||||
async fn list_routing_group_versions(
|
||||
&self,
|
||||
group_id: &str,
|
||||
) -> Result<Vec<StoredRoutingGroupVersion>, DataLayerError> {
|
||||
let sql = format!(
|
||||
"{ROUTING_GROUP_VERSION_SELECT} WHERE group_id = $1 ORDER BY version DESC, created_at DESC, id ASC"
|
||||
);
|
||||
let mut rows = sqlx::query(&sql).bind(group_id).fetch(&self.pool);
|
||||
let mut versions = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
versions.push(map_version_row(&row)?);
|
||||
}
|
||||
Ok(versions)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RoutingGroupWriteRepository for PostgresRoutingGroupRepository {
|
||||
async fn create_routing_group(
|
||||
&self,
|
||||
record: CreateRoutingGroupRecord,
|
||||
) -> Result<StoredRoutingGroup, DataLayerError> {
|
||||
let group = StoredRoutingGroup::new(record)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO routing_groups (
|
||||
id, name, description, enabled, is_system_default, config_json,
|
||||
version, created_at, updated_at, published_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
"#,
|
||||
)
|
||||
.bind(&group.id)
|
||||
.bind(&group.name)
|
||||
.bind(&group.description)
|
||||
.bind(group.enabled)
|
||||
.bind(group.is_system_default)
|
||||
.bind(&group.config_json)
|
||||
.bind(group.version)
|
||||
.bind(group.created_at)
|
||||
.bind(group.updated_at)
|
||||
.bind(group.published_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_routing_group(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: UpdateRoutingGroupRecord,
|
||||
) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
let Some(mut group) = self.reload_group(id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
apply_group_patch(&mut group, patch)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE routing_groups
|
||||
SET name = $2,
|
||||
description = $3,
|
||||
enabled = $4,
|
||||
is_system_default = $5,
|
||||
config_json = $6,
|
||||
version = $7,
|
||||
updated_at = $8,
|
||||
published_at = $9
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(&group.name)
|
||||
.bind(&group.description)
|
||||
.bind(group.enabled)
|
||||
.bind(group.is_system_default)
|
||||
.bind(&group.config_json)
|
||||
.bind(group.version)
|
||||
.bind(group.updated_at)
|
||||
.bind(group.published_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(Some(group))
|
||||
}
|
||||
|
||||
async fn delete_routing_group(&self, id: &str) -> Result<bool, DataLayerError> {
|
||||
let mut tx = self.pool.begin().await.map_postgres_err()?;
|
||||
sqlx::query("DELETE FROM routing_group_bindings WHERE group_id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
sqlx::query("DELETE FROM routing_group_versions WHERE group_id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let rows_affected = sqlx::query("DELETE FROM routing_groups WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_postgres_err()?
|
||||
.rows_affected();
|
||||
tx.commit().await.map_postgres_err()?;
|
||||
Ok(rows_affected > 0)
|
||||
}
|
||||
|
||||
async fn create_routing_group_binding(
|
||||
&self,
|
||||
record: CreateRoutingGroupBindingRecord,
|
||||
) -> Result<StoredRoutingGroupBinding, DataLayerError> {
|
||||
let binding = StoredRoutingGroupBinding::new(record)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO routing_group_bindings (
|
||||
id, group_id, subject_type, subject_id, is_default,
|
||||
allow_explicit_select, created_at, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
"#,
|
||||
)
|
||||
.bind(&binding.id)
|
||||
.bind(&binding.group_id)
|
||||
.bind(binding_subject_to_database(binding.subject_type))
|
||||
.bind(&binding.subject_id)
|
||||
.bind(binding.is_default)
|
||||
.bind(binding.allow_explicit_select)
|
||||
.bind(binding.created_at)
|
||||
.bind(binding.updated_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(binding)
|
||||
}
|
||||
|
||||
async fn delete_routing_group_binding(&self, id: &str) -> Result<bool, DataLayerError> {
|
||||
Ok(
|
||||
sqlx::query("DELETE FROM routing_group_bindings WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?
|
||||
.rows_affected()
|
||||
> 0,
|
||||
)
|
||||
}
|
||||
|
||||
async fn update_routing_group_binding(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: UpdateRoutingGroupBindingRecord,
|
||||
) -> Result<Option<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let Some(mut binding) = self.find_binding_by_id(id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
apply_binding_patch(&mut binding, patch)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE routing_group_bindings
|
||||
SET group_id = $2,
|
||||
subject_type = $3,
|
||||
subject_id = $4,
|
||||
is_default = $5,
|
||||
allow_explicit_select = $6,
|
||||
updated_at = $7
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(&binding.group_id)
|
||||
.bind(binding_subject_to_database(binding.subject_type))
|
||||
.bind(&binding.subject_id)
|
||||
.bind(binding.is_default)
|
||||
.bind(binding.allow_explicit_select)
|
||||
.bind(binding.updated_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(Some(binding))
|
||||
}
|
||||
|
||||
async fn create_routing_group_version(
|
||||
&self,
|
||||
record: CreateRoutingGroupVersionRecord,
|
||||
) -> Result<StoredRoutingGroupVersion, DataLayerError> {
|
||||
let version = StoredRoutingGroupVersion::new(record)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO routing_group_versions (
|
||||
id, group_id, version, config_json, created_at, created_by
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
"#,
|
||||
)
|
||||
.bind(&version.id)
|
||||
.bind(&version.group_id)
|
||||
.bind(version.version)
|
||||
.bind(&version.config_json)
|
||||
.bind(version.created_at)
|
||||
.bind(&version.created_by)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(version)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn apply_group_patch(
|
||||
group: &mut StoredRoutingGroup,
|
||||
patch: UpdateRoutingGroupRecord,
|
||||
) -> Result<(), DataLayerError> {
|
||||
if let Some(name) = patch.name {
|
||||
if name.trim().is_empty() {
|
||||
return Err(DataLayerError::InvalidInput(
|
||||
"routing_groups.name is empty".to_string(),
|
||||
));
|
||||
}
|
||||
group.name = name;
|
||||
}
|
||||
if let Some(description) = patch.description {
|
||||
group.description = description;
|
||||
}
|
||||
if let Some(enabled) = patch.enabled {
|
||||
group.enabled = enabled;
|
||||
}
|
||||
if let Some(is_system_default) = patch.is_system_default {
|
||||
group.is_system_default = is_system_default;
|
||||
}
|
||||
if let Some(config_json) = patch.config_json {
|
||||
if !config_json.is_object() {
|
||||
return Err(DataLayerError::InvalidInput(
|
||||
"routing_groups.config_json must be a JSON object".to_string(),
|
||||
));
|
||||
}
|
||||
group.config_json = config_json;
|
||||
}
|
||||
if let Some(version) = patch.version {
|
||||
group.version = version.max(1);
|
||||
}
|
||||
if let Some(published_at) = patch.published_at {
|
||||
group.published_at = published_at;
|
||||
}
|
||||
group.updated_at = patch.updated_at;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn apply_binding_patch(
|
||||
binding: &mut StoredRoutingGroupBinding,
|
||||
patch: UpdateRoutingGroupBindingRecord,
|
||||
) -> Result<(), DataLayerError> {
|
||||
if let Some(group_id) = patch.group_id {
|
||||
if group_id.trim().is_empty() {
|
||||
return Err(DataLayerError::InvalidInput(
|
||||
"routing_group_bindings.group_id is empty".to_string(),
|
||||
));
|
||||
}
|
||||
binding.group_id = group_id;
|
||||
}
|
||||
if let Some(subject_type) = patch.subject_type {
|
||||
binding.subject_type = subject_type;
|
||||
}
|
||||
if let Some(subject_id) = patch.subject_id {
|
||||
if subject_id.trim().is_empty() {
|
||||
return Err(DataLayerError::InvalidInput(
|
||||
"routing_group_bindings.subject_id is empty".to_string(),
|
||||
));
|
||||
}
|
||||
binding.subject_id = subject_id;
|
||||
}
|
||||
if let Some(is_default) = patch.is_default {
|
||||
binding.is_default = is_default;
|
||||
}
|
||||
if let Some(allow_explicit_select) = patch.allow_explicit_select {
|
||||
binding.allow_explicit_select = allow_explicit_select;
|
||||
}
|
||||
binding.updated_at = patch.updated_at;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn binding_subject_to_database(subject: RoutingGroupBindingSubject) -> &'static str {
|
||||
match subject {
|
||||
RoutingGroupBindingSubject::User => "user",
|
||||
RoutingGroupBindingSubject::ApiKey => "api_key",
|
||||
RoutingGroupBindingSubject::UserGroup => "user_group",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn binding_subject_from_database(
|
||||
value: String,
|
||||
) -> Result<RoutingGroupBindingSubject, DataLayerError> {
|
||||
match value.as_str() {
|
||||
"user" => Ok(RoutingGroupBindingSubject::User),
|
||||
"api_key" => Ok(RoutingGroupBindingSubject::ApiKey),
|
||||
"user_group" => Ok(RoutingGroupBindingSubject::UserGroup),
|
||||
_ => Err(DataLayerError::UnexpectedValue(format!(
|
||||
"invalid routing_group_bindings.subject_type: {value}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_group_row(row: &PgRow) -> Result<StoredRoutingGroup, DataLayerError> {
|
||||
Ok(StoredRoutingGroup {
|
||||
id: row.try_get("id").map_postgres_err()?,
|
||||
name: row.try_get("name").map_postgres_err()?,
|
||||
description: row.try_get("description").map_postgres_err()?,
|
||||
enabled: row.try_get("enabled").map_postgres_err()?,
|
||||
is_system_default: row.try_get("is_system_default").map_postgres_err()?,
|
||||
config_json: row.try_get("config_json").map_postgres_err()?,
|
||||
version: row.try_get("version").map_postgres_err()?,
|
||||
created_at: row.try_get("created_at").map_postgres_err()?,
|
||||
updated_at: row.try_get("updated_at").map_postgres_err()?,
|
||||
published_at: row.try_get("published_at").map_postgres_err()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_binding_row(row: &PgRow) -> Result<StoredRoutingGroupBinding, DataLayerError> {
|
||||
Ok(StoredRoutingGroupBinding {
|
||||
id: row.try_get("id").map_postgres_err()?,
|
||||
group_id: row.try_get("group_id").map_postgres_err()?,
|
||||
subject_type: binding_subject_from_database(
|
||||
row.try_get("subject_type").map_postgres_err()?,
|
||||
)?,
|
||||
subject_id: row.try_get("subject_id").map_postgres_err()?,
|
||||
is_default: row.try_get("is_default").map_postgres_err()?,
|
||||
allow_explicit_select: row.try_get("allow_explicit_select").map_postgres_err()?,
|
||||
created_at: row.try_get("created_at").map_postgres_err()?,
|
||||
updated_at: row.try_get("updated_at").map_postgres_err()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_version_row(row: &PgRow) -> Result<StoredRoutingGroupVersion, DataLayerError> {
|
||||
Ok(StoredRoutingGroupVersion {
|
||||
id: row.try_get("id").map_postgres_err()?,
|
||||
group_id: row.try_get("group_id").map_postgres_err()?,
|
||||
version: row.try_get("version").map_postgres_err()?,
|
||||
config_json: row.try_get("config_json").map_postgres_err()?,
|
||||
created_at: row.try_get("created_at").map_postgres_err()?,
|
||||
created_by: row.try_get("created_by").map_postgres_err()?,
|
||||
})
|
||||
}
|
||||
524
crates/aether-data/src/repository/routing_profiles/sqlite.rs
Normal file
524
crates/aether-data/src/repository/routing_profiles/sqlite.rs
Normal file
@@ -0,0 +1,524 @@
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use sqlx::{sqlite::SqliteRow, Row};
|
||||
|
||||
use super::postgres::{
|
||||
apply_binding_patch, apply_group_patch, binding_subject_from_database,
|
||||
binding_subject_to_database,
|
||||
};
|
||||
use super::*;
|
||||
use crate::driver::sqlite::SqlitePool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
|
||||
const ROUTING_GROUP_SELECT: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
is_system_default,
|
||||
config_json,
|
||||
version,
|
||||
created_at,
|
||||
updated_at,
|
||||
published_at
|
||||
FROM routing_groups
|
||||
"#;
|
||||
|
||||
const ROUTING_GROUP_BINDING_SELECT: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
group_id,
|
||||
subject_type,
|
||||
subject_id,
|
||||
is_default,
|
||||
allow_explicit_select,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM routing_group_bindings
|
||||
"#;
|
||||
|
||||
const ROUTING_GROUP_VERSION_SELECT: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
group_id,
|
||||
version,
|
||||
config_json,
|
||||
created_at,
|
||||
created_by
|
||||
FROM routing_group_versions
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SqliteRoutingGroupRepository {
|
||||
pool: SqlitePool,
|
||||
}
|
||||
|
||||
impl SqliteRoutingGroupRepository {
|
||||
pub fn new(pool: SqlitePool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
async fn reload_group(&self, id: &str) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
self.find_routing_group(RoutingGroupLookupKey::Id(id)).await
|
||||
}
|
||||
|
||||
async fn find_binding_by_id(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Option<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let row = sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_BINDING_SELECT} WHERE id = ? LIMIT 1"
|
||||
))
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
row.as_ref().map(map_binding_row).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RoutingGroupReadRepository for SqliteRoutingGroupRepository {
|
||||
async fn list_routing_groups(&self) -> Result<Vec<StoredRoutingGroup>, DataLayerError> {
|
||||
let rows = sqlx::query(&format!("{ROUTING_GROUP_SELECT} ORDER BY name ASC, id ASC"))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_group_row).collect()
|
||||
}
|
||||
|
||||
async fn find_routing_group(
|
||||
&self,
|
||||
lookup: RoutingGroupLookupKey<'_>,
|
||||
) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
let row = match lookup {
|
||||
RoutingGroupLookupKey::Id(id) => sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_SELECT} WHERE id = ? LIMIT 1"
|
||||
))
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?,
|
||||
RoutingGroupLookupKey::Name(name) => sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_SELECT} WHERE name = ? LIMIT 1"
|
||||
))
|
||||
.bind(name)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?,
|
||||
RoutingGroupLookupKey::SystemDefault => sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_SELECT} WHERE is_system_default = 1 AND enabled = 1 ORDER BY updated_at DESC, id ASC LIMIT 1"
|
||||
))
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?,
|
||||
};
|
||||
row.as_ref().map(map_group_row).transpose()
|
||||
}
|
||||
|
||||
async fn list_routing_group_bindings(
|
||||
&self,
|
||||
query: &RoutingGroupBindingQuery,
|
||||
) -> Result<Vec<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let rows = sqlx::query(&format!(
|
||||
r#"
|
||||
{ROUTING_GROUP_BINDING_SELECT}
|
||||
WHERE (? IS NULL OR group_id = ?)
|
||||
AND (? IS NULL OR subject_type = ?)
|
||||
AND (? IS NULL OR subject_id = ?)
|
||||
ORDER BY created_at ASC, id ASC
|
||||
"#
|
||||
))
|
||||
.bind(query.group_id.as_deref())
|
||||
.bind(query.group_id.as_deref())
|
||||
.bind(query.subject_type.map(binding_subject_to_database))
|
||||
.bind(query.subject_type.map(binding_subject_to_database))
|
||||
.bind(query.subject_id.as_deref())
|
||||
.bind(query.subject_id.as_deref())
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_binding_row).collect()
|
||||
}
|
||||
|
||||
async fn list_routing_group_versions(
|
||||
&self,
|
||||
group_id: &str,
|
||||
) -> Result<Vec<StoredRoutingGroupVersion>, DataLayerError> {
|
||||
let rows = sqlx::query(&format!(
|
||||
"{ROUTING_GROUP_VERSION_SELECT} WHERE group_id = ? ORDER BY version DESC, created_at DESC, id ASC"
|
||||
))
|
||||
.bind(group_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_version_row).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RoutingGroupWriteRepository for SqliteRoutingGroupRepository {
|
||||
async fn create_routing_group(
|
||||
&self,
|
||||
record: CreateRoutingGroupRecord,
|
||||
) -> Result<StoredRoutingGroup, DataLayerError> {
|
||||
let group = StoredRoutingGroup::new(record)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO routing_groups (
|
||||
id, name, description, enabled, is_system_default, config_json,
|
||||
version, created_at, updated_at, published_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&group.id)
|
||||
.bind(&group.name)
|
||||
.bind(&group.description)
|
||||
.bind(group.enabled)
|
||||
.bind(group.is_system_default)
|
||||
.bind(json_to_string(
|
||||
&group.config_json,
|
||||
"routing_groups.config_json",
|
||||
)?)
|
||||
.bind(group.version)
|
||||
.bind(group.created_at)
|
||||
.bind(group.updated_at)
|
||||
.bind(group.published_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_routing_group(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: UpdateRoutingGroupRecord,
|
||||
) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
|
||||
let Some(mut group) = self.reload_group(id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
apply_group_patch(&mut group, patch)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE routing_groups
|
||||
SET name = ?,
|
||||
description = ?,
|
||||
enabled = ?,
|
||||
is_system_default = ?,
|
||||
config_json = ?,
|
||||
version = ?,
|
||||
updated_at = ?,
|
||||
published_at = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(&group.name)
|
||||
.bind(&group.description)
|
||||
.bind(group.enabled)
|
||||
.bind(group.is_system_default)
|
||||
.bind(json_to_string(
|
||||
&group.config_json,
|
||||
"routing_groups.config_json",
|
||||
)?)
|
||||
.bind(group.version)
|
||||
.bind(group.updated_at)
|
||||
.bind(group.published_at)
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(Some(group))
|
||||
}
|
||||
|
||||
async fn delete_routing_group(&self, id: &str) -> Result<bool, DataLayerError> {
|
||||
let mut tx = self.pool.begin().await.map_sql_err()?;
|
||||
sqlx::query("DELETE FROM routing_group_bindings WHERE group_id = ?")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
sqlx::query("DELETE FROM routing_group_versions WHERE group_id = ?")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
let rows_affected = sqlx::query("DELETE FROM routing_groups WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?
|
||||
.rows_affected();
|
||||
tx.commit().await.map_sql_err()?;
|
||||
Ok(rows_affected > 0)
|
||||
}
|
||||
|
||||
async fn create_routing_group_binding(
|
||||
&self,
|
||||
record: CreateRoutingGroupBindingRecord,
|
||||
) -> Result<StoredRoutingGroupBinding, DataLayerError> {
|
||||
let binding = StoredRoutingGroupBinding::new(record)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO routing_group_bindings (
|
||||
id, group_id, subject_type, subject_id, is_default,
|
||||
allow_explicit_select, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&binding.id)
|
||||
.bind(&binding.group_id)
|
||||
.bind(binding_subject_to_database(binding.subject_type))
|
||||
.bind(&binding.subject_id)
|
||||
.bind(binding.is_default)
|
||||
.bind(binding.allow_explicit_select)
|
||||
.bind(binding.created_at)
|
||||
.bind(binding.updated_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(binding)
|
||||
}
|
||||
|
||||
async fn delete_routing_group_binding(&self, id: &str) -> Result<bool, DataLayerError> {
|
||||
Ok(
|
||||
sqlx::query("DELETE FROM routing_group_bindings WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?
|
||||
.rows_affected()
|
||||
> 0,
|
||||
)
|
||||
}
|
||||
|
||||
async fn update_routing_group_binding(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: UpdateRoutingGroupBindingRecord,
|
||||
) -> Result<Option<StoredRoutingGroupBinding>, DataLayerError> {
|
||||
let Some(mut binding) = self.find_binding_by_id(id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
apply_binding_patch(&mut binding, patch)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE routing_group_bindings
|
||||
SET group_id = ?,
|
||||
subject_type = ?,
|
||||
subject_id = ?,
|
||||
is_default = ?,
|
||||
allow_explicit_select = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(&binding.group_id)
|
||||
.bind(binding_subject_to_database(binding.subject_type))
|
||||
.bind(&binding.subject_id)
|
||||
.bind(binding.is_default)
|
||||
.bind(binding.allow_explicit_select)
|
||||
.bind(binding.updated_at)
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(Some(binding))
|
||||
}
|
||||
|
||||
async fn create_routing_group_version(
|
||||
&self,
|
||||
record: CreateRoutingGroupVersionRecord,
|
||||
) -> Result<StoredRoutingGroupVersion, DataLayerError> {
|
||||
let version = StoredRoutingGroupVersion::new(record)?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO routing_group_versions (
|
||||
id, group_id, version, config_json, created_at, created_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&version.id)
|
||||
.bind(&version.group_id)
|
||||
.bind(version.version)
|
||||
.bind(json_to_string(
|
||||
&version.config_json,
|
||||
"routing_group_versions.config_json",
|
||||
)?)
|
||||
.bind(version.created_at)
|
||||
.bind(&version.created_by)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(version)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_group_row(row: &SqliteRow) -> Result<StoredRoutingGroup, DataLayerError> {
|
||||
Ok(StoredRoutingGroup {
|
||||
id: row.try_get("id").map_sql_err()?,
|
||||
name: row.try_get("name").map_sql_err()?,
|
||||
description: row.try_get("description").map_sql_err()?,
|
||||
enabled: row.try_get("enabled").map_sql_err()?,
|
||||
is_system_default: row.try_get("is_system_default").map_sql_err()?,
|
||||
config_json: json_from_string(
|
||||
row.try_get("config_json").map_sql_err()?,
|
||||
"routing_groups.config_json",
|
||||
)?,
|
||||
version: row.try_get("version").map_sql_err()?,
|
||||
created_at: row.try_get("created_at").map_sql_err()?,
|
||||
updated_at: row.try_get("updated_at").map_sql_err()?,
|
||||
published_at: row.try_get("published_at").map_sql_err()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_binding_row(row: &SqliteRow) -> Result<StoredRoutingGroupBinding, DataLayerError> {
|
||||
Ok(StoredRoutingGroupBinding {
|
||||
id: row.try_get("id").map_sql_err()?,
|
||||
group_id: row.try_get("group_id").map_sql_err()?,
|
||||
subject_type: binding_subject_from_database(row.try_get("subject_type").map_sql_err()?)?,
|
||||
subject_id: row.try_get("subject_id").map_sql_err()?,
|
||||
is_default: row.try_get("is_default").map_sql_err()?,
|
||||
allow_explicit_select: row.try_get("allow_explicit_select").map_sql_err()?,
|
||||
created_at: row.try_get("created_at").map_sql_err()?,
|
||||
updated_at: row.try_get("updated_at").map_sql_err()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_version_row(row: &SqliteRow) -> Result<StoredRoutingGroupVersion, DataLayerError> {
|
||||
Ok(StoredRoutingGroupVersion {
|
||||
id: row.try_get("id").map_sql_err()?,
|
||||
group_id: row.try_get("group_id").map_sql_err()?,
|
||||
version: row.try_get("version").map_sql_err()?,
|
||||
config_json: json_from_string(
|
||||
row.try_get("config_json").map_sql_err()?,
|
||||
"routing_group_versions.config_json",
|
||||
)?,
|
||||
created_at: row.try_get("created_at").map_sql_err()?,
|
||||
created_by: row.try_get("created_by").map_sql_err()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn json_to_string(value: &Value, field_name: &str) -> Result<String, DataLayerError> {
|
||||
serde_json::to_string(value).map_err(|err| {
|
||||
DataLayerError::UnexpectedValue(format!("{field_name} contains unserializable JSON: {err}"))
|
||||
})
|
||||
}
|
||||
|
||||
fn json_from_string(value: String, field_name: &str) -> Result<Value, DataLayerError> {
|
||||
serde_json::from_str(&value).map_err(|err| {
|
||||
DataLayerError::UnexpectedValue(format!("{field_name} contains invalid JSON: {err}"))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::lifecycle::migrate::run_sqlite_migrations;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sqlite_routing_group_repository_round_trips() {
|
||||
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect("sqlite::memory:")
|
||||
.await
|
||||
.expect("sqlite pool should connect");
|
||||
run_sqlite_migrations(&pool)
|
||||
.await
|
||||
.expect("sqlite migrations should run");
|
||||
|
||||
let repository = SqliteRoutingGroupRepository::new(pool);
|
||||
repository
|
||||
.create_routing_group(CreateRoutingGroupRecord {
|
||||
id: "routing-group-1".to_string(),
|
||||
name: "default".to_string(),
|
||||
description: Some("initial".to_string()),
|
||||
enabled: true,
|
||||
is_system_default: true,
|
||||
config_json: json!({"allowed_models": ["gpt-*"]}),
|
||||
version: 1,
|
||||
created_at: 10,
|
||||
updated_at: 10,
|
||||
published_at: None,
|
||||
})
|
||||
.await
|
||||
.expect("group should create");
|
||||
|
||||
let system_default = repository
|
||||
.find_routing_group(RoutingGroupLookupKey::SystemDefault)
|
||||
.await
|
||||
.expect("group lookup should succeed")
|
||||
.expect("system default should exist");
|
||||
assert_eq!(system_default.id, "routing-group-1");
|
||||
|
||||
repository
|
||||
.update_routing_group(
|
||||
"routing-group-1",
|
||||
UpdateRoutingGroupRecord {
|
||||
description: Some(None),
|
||||
version: Some(2),
|
||||
updated_at: 20,
|
||||
published_at: Some(Some(20)),
|
||||
..UpdateRoutingGroupRecord::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("group should update");
|
||||
|
||||
let binding = repository
|
||||
.create_routing_group_binding(CreateRoutingGroupBindingRecord {
|
||||
id: "binding-1".to_string(),
|
||||
group_id: "routing-group-1".to_string(),
|
||||
subject_type: RoutingGroupBindingSubject::ApiKey,
|
||||
subject_id: "api-key-1".to_string(),
|
||||
is_default: true,
|
||||
allow_explicit_select: true,
|
||||
created_at: 10,
|
||||
updated_at: 10,
|
||||
})
|
||||
.await
|
||||
.expect("binding should create");
|
||||
|
||||
assert_eq!(binding.subject_type, RoutingGroupBindingSubject::ApiKey);
|
||||
assert_eq!(
|
||||
repository
|
||||
.list_routing_group_bindings(&RoutingGroupBindingQuery {
|
||||
group_id: Some("routing-group-1".to_string()),
|
||||
subject_type: Some(RoutingGroupBindingSubject::ApiKey),
|
||||
subject_id: Some("api-key-1".to_string()),
|
||||
})
|
||||
.await
|
||||
.expect("bindings should list")
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
repository
|
||||
.create_routing_group_version(CreateRoutingGroupVersionRecord {
|
||||
id: "version-1".to_string(),
|
||||
group_id: "routing-group-1".to_string(),
|
||||
version: 2,
|
||||
config_json: json!({"allowed_models": ["gpt-*"]}),
|
||||
created_at: 20,
|
||||
created_by: Some("admin".to_string()),
|
||||
})
|
||||
.await
|
||||
.expect("version should create");
|
||||
|
||||
assert_eq!(
|
||||
repository
|
||||
.list_routing_group_versions("routing-group-1")
|
||||
.await
|
||||
.expect("versions should list")
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user