feat: Implement database migration framework with initial migrations for organizations, workspaces, agents, virtual hosts, upstreams, certificates, and users

This commit is contained in:
GW_MC
2026-03-03 04:31:06 +00:00
parent 2e9ad4fc21
commit 7d9285ba44
15 changed files with 785 additions and 23 deletions

35
Cargo.lock generated
View File

@@ -1708,7 +1708,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
"webpki-roots", "webpki-roots 1.0.6",
] ]
[[package]] [[package]]
@@ -2225,6 +2225,14 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "migration"
version = "0.1.0"
dependencies = [
"async-std",
"sea-orm-migration",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -2480,11 +2488,12 @@ dependencies = [
"handlebars", "handlebars",
"hex", "hex",
"jsonwebtoken", "jsonwebtoken",
"migration",
"mockall", "mockall",
"nxmesh-core", "nxmesh-core",
"nxmesh-proto", "nxmesh-proto",
"sea-orm", "sea-orm",
"sea-orm-migration 2.0.0-rc.35", "sea-orm-migration",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@@ -3272,7 +3281,7 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots 1.0.6",
] ]
[[package]] [[package]]
@@ -3593,15 +3602,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "sea-orm-migration"
version = "0.1.0"
dependencies = [
"async-std",
"sea-orm",
"sea-orm-migration 2.0.0-rc.35",
]
[[package]] [[package]]
name = "sea-orm-migration" name = "sea-orm-migration"
version = "2.0.0-rc.35" version = "2.0.0-rc.35"
@@ -3961,6 +3961,7 @@ dependencies = [
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"rust_decimal", "rust_decimal",
"rustls",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@@ -3972,6 +3973,7 @@ dependencies = [
"tracing", "tracing",
"url", "url",
"uuid", "uuid",
"webpki-roots 0.26.11",
] ]
[[package]] [[package]]
@@ -5075,6 +5077,15 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.6",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.6" version = "1.0.6"

View File

@@ -5,7 +5,7 @@ members = [
"crates/nxmesh-master", "crates/nxmesh-master",
"crates/nxmesh-agent", "crates/nxmesh-agent",
"crates/nxmesh-cli", "crates/nxmesh-cli",
"migrations/sea-orm", "migration",
] ]
resolver = "3" resolver = "3"
@@ -36,7 +36,10 @@ tonic = "0.11"
prost = "0.12" prost = "0.12"
# Database # Database
sea-orm = { version = "2.0.0-rc", features = ["sqlx-postgres", "runtime-tokio-native-tls"] } sea-orm = { version = "2.0.0-rc", features = [
"sqlx-postgres",
"runtime-tokio-native-tls",
] }
sea-orm-migration = "2.0.0-rc" sea-orm-migration = "2.0.0-rc"
# Async # Async
@@ -48,7 +51,10 @@ toml = "0.8"
config = "0.14" config = "0.14"
# HTTP client # HTTP client
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls",
"json",
] }
# Crypto # Crypto
sha2 = "0.10" sha2 = "0.10"

View File

@@ -173,8 +173,7 @@ nxmesh/
│ # Build output (dist/) is embedded into master binary │ # Build output (dist/) is embedded into master binary
│ # Master serves static files at root path ("/") │ # Master serves static files at root path ("/")
├── migrations/ # Database migrations ├── migration/ # Database migrations
│ └── sea-orm/
│ ├── Cargo.toml │ ├── Cargo.toml
│ └── src/ │ └── src/

View File

@@ -115,6 +115,17 @@ db-console:
@echo "🐘 Connecting to PostgreSQL..." @echo "🐘 Connecting to PostgreSQL..."
psql $DATABASE_URL psql $DATABASE_URL
# Generate SeaORM entities from database schema
db-generate:
@echo "⚙️ Generating SeaORM entities from database..."
sea-orm-cli generate entity \
--database-url $DATABASE_URL \
--output-dir crates/nxmesh-master/src/db/entities \
--with-serde both \
--with-copy-enums \
--date-time-crate chrono
@echo "✅ Entities generated at crates/nxmesh-master/src/db/entities/"
# ============================================================================= # =============================================================================
# Testing Commands # Testing Commands
# ============================================================================= # =============================================================================
@@ -195,6 +206,7 @@ nginx-reload:
bash .devcontainer/scripts/nginx-reload.sh bash .devcontainer/scripts/nginx-reload.sh
# Full nginx control via shared PID namespace # Full nginx control via shared PID namespace
# Usage: just nginx-ctl <reload|stop|quit|reopen|upgrade|status> # Usage: just nginx-ctl <reload|stop|quit|reopen|upgrade|status>
nginx-ctl cmd="reload": nginx-ctl cmd="reload":
@bash .devcontainer/scripts/nginx-ctl.sh {{ cmd }} @bash .devcontainer/scripts/nginx-ctl.sh {{ cmd }}
@@ -264,3 +276,37 @@ logs:
docs: docs:
@echo "📚 Generating documentation..." @echo "📚 Generating documentation..."
cargo doc --open 2>/dev/null || cargo doc cargo doc --open 2>/dev/null || cargo doc
# =============================================================================
# API Client Generation Commands
# =============================================================================
# Generate OpenAPI spec from backend (requires backend to be running)
gen-openapi:
@echo "📋 Generating OpenAPI spec from backend..."
@curl -s http://localhost:8080/api/openapi.json > /tmp/openapi.json
@echo "✅ OpenAPI spec saved to /tmp/openapi.json"
# Generate TypeScript API client from OpenAPI spec
gen-api-client:
@echo "⚡ Generating TypeScript API client..."
@if [ ! -f /tmp/openapi.json ]; then \
echo "❌ OpenAPI spec not found. Run 'just gen-openapi' first (backend must be running)."; \
exit 1; \
fi
cd frontend && bunx openapi-typescript /tmp/openapi.json -o src/api/schema.ts
@echo "✅ API client generated at frontend/src/api/schema.ts"
# Full API generation workflow (start backend, generate spec, generate client, stop backend)
gen-api: start-backend-temp
@just gen-openapi
@just gen-api-client
@echo "✅ API client generation complete!"
# Internal: Start backend temporarily for API generation
start-backend-temp:
@echo "🔧 Starting backend temporarily..."
@cargo build --package nxmesh-master
@cargo run --package nxmesh-master &
@sleep 5
@echo "✅ Backend ready"

19
migration/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
[dependencies.sea-orm-migration]
version = "2.0.0-rc"
features = [
"runtime-tokio-rustls",
"sqlx-postgres",
]

41
migration/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

26
migration/src/lib.rs Normal file
View File

@@ -0,0 +1,26 @@
pub use sea_orm_migration::prelude::*;
mod m20240301_000001_create_organizations;
mod m20240301_000002_create_workspaces;
mod m20240301_000003_create_agents;
mod m20240301_000004_create_virtual_hosts;
mod m20240301_000005_create_upstreams;
mod m20240301_000006_create_certificates;
mod m20240301_000007_create_users;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20240301_000001_create_organizations::Migration),
Box::new(m20240301_000002_create_workspaces::Migration),
Box::new(m20240301_000003_create_agents::Migration),
Box::new(m20240301_000004_create_virtual_hosts::Migration),
Box::new(m20240301_000005_create_upstreams::Migration),
Box::new(m20240301_000006_create_certificates::Migration),
Box::new(m20240301_000007_create_users::Migration),
]
}
}

View File

@@ -0,0 +1,59 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Organizations::Table)
.if_not_exists()
.col(
ColumnDef::new(Organizations::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Organizations::Name).string().not_null())
.col(
ColumnDef::new(Organizations::Slug)
.string()
.not_null()
.unique_key(),
)
.col(
ColumnDef::new(Organizations::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Organizations::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(ColumnDef::new(Organizations::Settings).json())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Organizations::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Organizations {
Table,
Id,
Name,
Slug,
CreatedAt,
UpdatedAt,
Settings,
}

View File

@@ -0,0 +1,88 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Workspaces::Table)
.if_not_exists()
.col(
ColumnDef::new(Workspaces::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Workspaces::OrganizationId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(Workspaces::Name).string().not_null())
.col(
ColumnDef::new(Workspaces::Slug)
.string()
.not_null(),
)
.col(
ColumnDef::new(Workspaces::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Workspaces::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_workspace_organization")
.from(Workspaces::Table, Workspaces::OrganizationId)
.to(Organizations::Table, Organizations::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await?;
// Create unique index on organization_id + slug
manager
.create_index(
Index::create()
.unique()
.name("idx_workspaces_org_slug")
.table(Workspaces::Table)
.col(Workspaces::OrganizationId)
.col(Workspaces::Slug)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Workspaces::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
OrganizationId,
Name,
Slug,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Organizations {
Table,
Id,
}

View File

@@ -0,0 +1,90 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Agents::Table)
.if_not_exists()
.col(
ColumnDef::new(Agents::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Agents::WorkspaceId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(Agents::Name).string().not_null())
.col(ColumnDef::new(Agents::Hostname).string().not_null())
.col(ColumnDef::new(Agents::IpAddress).string())
.col(ColumnDef::new(Agents::Version).string())
.col(ColumnDef::new(Agents::State).string().not_null())
.col(ColumnDef::new(Agents::DeploymentMode).string())
.col(
ColumnDef::new(Agents::LastSeenAt)
.timestamp_with_time_zone(),
)
.col(ColumnDef::new(Agents::Capabilities).json())
.col(ColumnDef::new(Agents::Labels).json())
.col(ColumnDef::new(Agents::TokenHash).string())
.col(
ColumnDef::new(Agents::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Agents::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_agent_workspace")
.from(Agents::Table, Agents::WorkspaceId)
.to(Workspaces::Table, Workspaces::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Agents::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Agents {
Table,
Id,
WorkspaceId,
Name,
Hostname,
IpAddress,
Version,
State,
DeploymentMode,
LastSeenAt,
Capabilities,
Labels,
TokenHash,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
}

View File

@@ -0,0 +1,115 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(VirtualHosts::Table)
.if_not_exists()
.col(
ColumnDef::new(VirtualHosts::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(VirtualHosts::WorkspaceId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(VirtualHosts::Name).string().not_null())
.col(
ColumnDef::new(VirtualHosts::ServerName)
.string()
.not_null(),
)
.col(
ColumnDef::new(VirtualHosts::ListenPort)
.integer()
.not_null(),
)
.col(
ColumnDef::new(VirtualHosts::SslEnabled)
.boolean()
.not_null()
.default(false),
)
.col(ColumnDef::new(VirtualHosts::SslCertificateId).uuid())
.col(ColumnDef::new(VirtualHosts::Locations).json())
.col(
ColumnDef::new(VirtualHosts::Http2Enabled)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(VirtualHosts::Http3Enabled)
.boolean()
.not_null()
.default(false),
)
.col(
ColumnDef::new(VirtualHosts::GzipEnabled)
.boolean()
.not_null()
.default(true),
)
.col(ColumnDef::new(VirtualHosts::TargetAgents).json())
.col(
ColumnDef::new(VirtualHosts::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(VirtualHosts::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_vh_workspace")
.from(VirtualHosts::Table, VirtualHosts::WorkspaceId)
.to(Workspaces::Table, Workspaces::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(VirtualHosts::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum VirtualHosts {
Table,
Id,
WorkspaceId,
Name,
ServerName,
ListenPort,
SslEnabled,
SslCertificateId,
Locations,
Http2Enabled,
Http3Enabled,
GzipEnabled,
TargetAgents,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
}

View File

@@ -0,0 +1,83 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Upstreams::Table)
.if_not_exists()
.col(
ColumnDef::new(Upstreams::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Upstreams::WorkspaceId)
.uuid()
.not_null(),
)
.col(ColumnDef::new(Upstreams::Name).string().not_null())
.col(
ColumnDef::new(Upstreams::Algorithm)
.string()
.not_null(),
)
.col(ColumnDef::new(Upstreams::Servers).json())
.col(ColumnDef::new(Upstreams::HealthCheck).json())
.col(ColumnDef::new(Upstreams::KeepaliveConnections).integer())
.col(ColumnDef::new(Upstreams::KeepaliveTimeout).integer())
.col(
ColumnDef::new(Upstreams::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Upstreams::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_upstream_workspace")
.from(Upstreams::Table, Upstreams::WorkspaceId)
.to(Workspaces::Table, Workspaces::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Upstreams::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Upstreams {
Table,
Id,
WorkspaceId,
Name,
Algorithm,
Servers,
HealthCheck,
KeepaliveConnections,
KeepaliveTimeout,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
}

View File

@@ -0,0 +1,99 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Certificates::Table)
.if_not_exists()
.col(
ColumnDef::new(Certificates::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(Certificates::WorkspaceId)
.uuid()
.not_null(),
)
.col(
ColumnDef::new(Certificates::Domain)
.string()
.not_null(),
)
.col(
ColumnDef::new(Certificates::IsWildcard)
.boolean()
.not_null()
.default(false),
)
.col(ColumnDef::new(Certificates::Provider).string())
.col(ColumnDef::new(Certificates::Status).string())
.col(ColumnDef::new(Certificates::IssuedAt).timestamp_with_time_zone())
.col(ColumnDef::new(Certificates::ExpiresAt).timestamp_with_time_zone())
.col(
ColumnDef::new(Certificates::AutoRenew)
.boolean()
.not_null()
.default(true),
)
.col(ColumnDef::new(Certificates::CertificatePem).text())
.col(ColumnDef::new(Certificates::PrivateKeyPem).text())
.col(
ColumnDef::new(Certificates::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Certificates::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_certificate_workspace")
.from(Certificates::Table, Certificates::WorkspaceId)
.to(Workspaces::Table, Workspaces::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Certificates::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Certificates {
Table,
Id,
WorkspaceId,
Domain,
IsWildcard,
Provider,
Status,
IssuedAt,
ExpiresAt,
AutoRenew,
CertificatePem,
PrivateKeyPem,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Workspaces {
Table,
Id,
}

View File

@@ -0,0 +1,74 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Users::Table)
.if_not_exists()
.col(
ColumnDef::new(Users::Id)
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Users::Email).string().not_null().unique_key())
.col(ColumnDef::new(Users::PasswordHash).string().not_null())
.col(ColumnDef::new(Users::Name).string())
.col(ColumnDef::new(Users::Role).string().not_null())
.col(
ColumnDef::new(Users::OrganizationId)
.uuid(),
)
.col(
ColumnDef::new(Users::CreatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.col(
ColumnDef::new(Users::UpdatedAt)
.timestamp_with_time_zone()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_user_organization")
.from(Users::Table, Users::OrganizationId)
.to(Organizations::Table, Organizations::Id)
.on_delete(ForeignKeyAction::SetNull),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Users::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
Email,
PasswordHash,
Name,
Role,
OrganizationId,
CreatedAt,
UpdatedAt,
}
#[derive(DeriveIden)]
enum Organizations {
Table,
Id,
}

6
migration/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}