mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
refactor(ci): switch 1Panel appstore to community repo and manual trigger
This commit is contained in:
@@ -1,46 +1,41 @@
|
||||
name: 1Panel App Store
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published, released]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to submit (e.g. 0.5.3)"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
submit:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.release.draft && !github.event.release.prerelease }}
|
||||
env:
|
||||
REPO_OWNER: t8y2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
VERSION="${VERSION#v}"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Submit to 1Panel App Store
|
||||
env:
|
||||
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
|
||||
git clone --depth 1 \
|
||||
"https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/${REPO_OWNER}/appstore.git" \
|
||||
git clone --depth 1 -b localApps \
|
||||
"https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/${REPO_OWNER}/appstore-1panel.git" \
|
||||
appstore
|
||||
cd appstore
|
||||
|
||||
APP_DIR="apps/dbx"
|
||||
mkdir -p "${APP_DIR}/${VERSION}"
|
||||
mkdir -p "${APP_DIR}/latest"
|
||||
|
||||
cp "${{ github.workspace }}/1panel/data.yml" "${APP_DIR}/data.yml"
|
||||
cp "${{ github.workspace }}/1panel/README.md" "${APP_DIR}/README.md"
|
||||
if [ -f "${{ github.workspace }}/1panel/logo.png" ]; then
|
||||
cp "${{ github.workspace }}/1panel/logo.png" "${APP_DIR}/logo.png"
|
||||
fi
|
||||
cp -r "${{ github.workspace }}/1panel/latest/." "${APP_DIR}/${VERSION}/"
|
||||
cp -r "${{ github.workspace }}/1panel/latest/." "${APP_DIR}/latest/"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
@@ -55,10 +50,10 @@ jobs:
|
||||
git commit -m "feat(dbx): release ${VERSION}"
|
||||
git push origin "${BRANCH}"
|
||||
gh pr create \
|
||||
--repo "1Panel-dev/appstore" \
|
||||
--repo "okxlin/appstore" \
|
||||
--head "${REPO_OWNER}:${BRANCH}" \
|
||||
--title "feat(dbx): release ${VERSION}" \
|
||||
--body "$(printf 'Automated release of DBX v%s\n\n```release-note\nfeat(dbx): release %s\n```' "${VERSION}" "${VERSION}")" \
|
||||
--base dev
|
||||
--body "Automated release of DBX v${VERSION}" \
|
||||
--base localApps
|
||||
echo "::notice::1Panel App Store PR created for ${VERSION}"
|
||||
fi
|
||||
|
||||
+12
-1
@@ -1,13 +1,24 @@
|
||||
additional:
|
||||
name: DBX
|
||||
tags:
|
||||
- 工具
|
||||
title: 轻量级数据库管理工具
|
||||
description: 轻量级数据库管理工具,支持 25+ 种数据库
|
||||
additionalProperties:
|
||||
key: dbx
|
||||
name: DBX
|
||||
tags:
|
||||
- Tool
|
||||
shortDescZh: 轻量级数据库管理工具,支持 25+ 种数据库
|
||||
shortDescEn: Lightweight database manager supporting 25+ databases
|
||||
description:
|
||||
en: Lightweight database manager supporting 25+ databases
|
||||
zh: 轻量级数据库管理工具,支持 25+ 种数据库
|
||||
type: tool
|
||||
crossVersionUpdate: true
|
||||
limit: 0
|
||||
recommend: 0
|
||||
website: https://github.com/t8y2/dbx
|
||||
github: https://github.com/t8y2/dbx
|
||||
document: https://github.com/t8y2/dbx#readme
|
||||
architectures:
|
||||
- amd64
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
additionalProperties:
|
||||
formFields:
|
||||
- default: 4224
|
||||
edit: true
|
||||
envKey: PANEL_APP_PORT_HTTP
|
||||
labelZh: 端口
|
||||
labelEn: Port
|
||||
label:
|
||||
zh: 端口
|
||||
en: Port
|
||||
@@ -10,7 +13,10 @@ additionalProperties:
|
||||
type: number
|
||||
|
||||
- default: dbx_password
|
||||
edit: true
|
||||
envKey: PANEL_DB_PASSWORD
|
||||
labelZh: 访问密码
|
||||
labelEn: Access Password
|
||||
label:
|
||||
zh: 访问密码
|
||||
en: Access Password
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
${CONTAINER_NAME}:
|
||||
image: t8y2/dbx:${PANEL_APP_VERSION_TAG}
|
||||
dbx:
|
||||
image: t8y2/dbx:latest
|
||||
container_name: ${CONTAINER_NAME}
|
||||
ports:
|
||||
- ${PANEL_APP_PORT_HTTP}:4224
|
||||
- "${PANEL_APP_PORT_HTTP}:4224"
|
||||
environment:
|
||||
- DBX_PASSWORD=${PANEL_DB_PASSWORD}
|
||||
volumes:
|
||||
@@ -13,6 +11,8 @@ services:
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- 1panel-network
|
||||
labels:
|
||||
createdBy: "Apps"
|
||||
|
||||
networks:
|
||||
1panel-network:
|
||||
|
||||
@@ -677,6 +677,217 @@ pub async fn get_table_ddl_core(
|
||||
}
|
||||
}
|
||||
|
||||
fn sql_string(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "''"))
|
||||
}
|
||||
|
||||
fn pg_ident(value: &str) -> String {
|
||||
format!("\"{}\"", value.replace('"', "\"\""))
|
||||
}
|
||||
|
||||
fn mysql_ident(value: &str) -> String {
|
||||
format!("`{}`", value.replace('`', "``"))
|
||||
}
|
||||
|
||||
fn sqlite_object_type(kind: &db::ObjectSourceKind) -> &'static str {
|
||||
match kind {
|
||||
db::ObjectSourceKind::View => "view",
|
||||
db::ObjectSourceKind::Procedure | db::ObjectSourceKind::Function => "routine",
|
||||
}
|
||||
}
|
||||
|
||||
fn sqlserver_object_type_filter(kind: &db::ObjectSourceKind) -> &'static str {
|
||||
match kind {
|
||||
db::ObjectSourceKind::View => "'V'",
|
||||
db::ObjectSourceKind::Procedure => "'P'",
|
||||
db::ObjectSourceKind::Function => "'FN','IF','TF','FS','FT'",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sqlserver_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourceKind) -> String {
|
||||
format!(
|
||||
"SELECT m.definition FROM sys.sql_modules m \
|
||||
JOIN sys.objects o ON o.object_id = m.object_id \
|
||||
JOIN sys.schemas s ON s.schema_id = o.schema_id \
|
||||
WHERE s.name = {} AND o.name = {} AND o.type IN ({})",
|
||||
sql_string(schema),
|
||||
sql_string(name),
|
||||
sqlserver_object_type_filter(kind)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn postgres_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourceKind) -> String {
|
||||
match kind {
|
||||
db::ObjectSourceKind::View => {
|
||||
format!("SELECT pg_get_viewdef('{}.{}'::regclass, true)", pg_ident(schema), pg_ident(name))
|
||||
}
|
||||
db::ObjectSourceKind::Procedure | db::ObjectSourceKind::Function => {
|
||||
let prokind = if matches!(kind, db::ObjectSourceKind::Procedure) { "p" } else { "f" };
|
||||
format!(
|
||||
"SELECT pg_get_functiondef(p.oid) \
|
||||
FROM pg_proc p \
|
||||
JOIN pg_namespace n ON n.oid = p.pronamespace \
|
||||
WHERE n.nspname = {} AND p.proname = {} AND p.prokind = '{}' \
|
||||
ORDER BY p.oid LIMIT 1",
|
||||
sql_string(schema),
|
||||
sql_string(name),
|
||||
prokind
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn oracle_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourceKind) -> String {
|
||||
let object_type = match kind {
|
||||
db::ObjectSourceKind::View => "VIEW",
|
||||
db::ObjectSourceKind::Procedure => "PROCEDURE",
|
||||
db::ObjectSourceKind::Function => "FUNCTION",
|
||||
};
|
||||
format!(
|
||||
"SELECT DBMS_METADATA.GET_DDL({}, {}, {}) FROM DUAL",
|
||||
sql_string(object_type),
|
||||
sql_string(name),
|
||||
sql_string(schema)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sqlite_object_source_sql(name: &str, kind: &db::ObjectSourceKind) -> String {
|
||||
format!(
|
||||
"SELECT sql FROM sqlite_master WHERE type = {} AND name = {}",
|
||||
sql_string(sqlite_object_type(kind)),
|
||||
sql_string(name)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn mysql_object_source_sql(name: &str, kind: &db::ObjectSourceKind) -> String {
|
||||
match kind {
|
||||
db::ObjectSourceKind::View => format!("SHOW CREATE VIEW {}", mysql_ident(name)),
|
||||
db::ObjectSourceKind::Procedure => format!("SHOW CREATE PROCEDURE {}", mysql_ident(name)),
|
||||
db::ObjectSourceKind::Function => format!("SHOW CREATE FUNCTION {}", mysql_ident(name)),
|
||||
}
|
||||
}
|
||||
|
||||
fn first_string_cell(result: db::QueryResult) -> Result<String, String> {
|
||||
result
|
||||
.rows
|
||||
.first()
|
||||
.and_then(|row| row.iter().find_map(|value| value.as_str().map(str::to_string)))
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| "Object source not found".to_string())
|
||||
}
|
||||
|
||||
async fn mysql_object_source(
|
||||
pool: &sqlx::mysql::MySqlPool,
|
||||
name: &str,
|
||||
kind: &db::ObjectSourceKind,
|
||||
) -> Result<String, String> {
|
||||
use sqlx::Row;
|
||||
let sql = mysql_object_source_sql(name, kind);
|
||||
let row: sqlx::mysql::MySqlRow = sqlx::raw_sql(&sql).fetch_one(pool).await.map_err(|e| e.to_string())?;
|
||||
let index = if matches!(kind, db::ObjectSourceKind::View) { 1 } else { 2 };
|
||||
row.try_get::<String, _>(index)
|
||||
.or_else(|_| row.try_get::<Vec<u8>, _>(index).map(|b| String::from_utf8_lossy(&b).to_string()))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_object_source_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
name: &str,
|
||||
object_type: db::ObjectSourceKind,
|
||||
) -> Result<db::ObjectSource, String> {
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
let source = {
|
||||
let connections = state.connections.read().await;
|
||||
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
first_string_cell(
|
||||
db::sqlserver::execute_query(&mut client, &sqlserver_object_source_sql(schema, name, &object_type))
|
||||
.await?,
|
||||
)?
|
||||
} else if let Some(pool) = extract_oracle(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let client = pool.client();
|
||||
let client = client.lock().await;
|
||||
first_string_cell(
|
||||
db::oracle_driver::execute_query(&*client, &oracle_object_source_sql(schema, name, &object_type))
|
||||
.await?,
|
||||
)?
|
||||
} else if let Some(client) = extract_gaussdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let mut client = client.lock().await;
|
||||
first_string_cell(
|
||||
db::gaussdb_driver::execute_query(&mut client, &postgres_object_source_sql(schema, name, &object_type))
|
||||
.await?,
|
||||
)?
|
||||
} else {
|
||||
match connections.get(&pool_key).ok_or("Pool not found")? {
|
||||
PoolKind::Mysql(pool, _) => mysql_object_source(pool, name, &object_type).await?,
|
||||
PoolKind::Postgres(pool) => first_string_cell(
|
||||
db::postgres::execute_query(pool, &postgres_object_source_sql(schema, name, &object_type)).await?,
|
||||
)?,
|
||||
PoolKind::Sqlite(pool) => first_string_cell(
|
||||
db::sqlite::execute_query(pool, &sqlite_object_source_sql(name, &object_type)).await?,
|
||||
)?,
|
||||
PoolKind::ClickHouse(client) if matches!(object_type, db::ObjectSourceKind::View) => {
|
||||
let result = db::clickhouse_driver::execute_query(
|
||||
client,
|
||||
database,
|
||||
&format!("SHOW CREATE TABLE {}", mysql_ident(name)),
|
||||
)
|
||||
.await?;
|
||||
first_string_cell(result)?
|
||||
}
|
||||
_ => return Err("Object source is not supported for this database type".to_string()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(db::ObjectSource {
|
||||
name: name.to_string(),
|
||||
object_type,
|
||||
schema: if schema.is_empty() { None } else { Some(schema.to_string()) },
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod object_source_tests {
|
||||
use super::*;
|
||||
use crate::types::ObjectSourceKind;
|
||||
|
||||
#[test]
|
||||
fn builds_sqlserver_object_source_sql_for_schema_scoped_routines() {
|
||||
assert_eq!(
|
||||
sqlserver_object_source_sql("dbo", "refresh_cache", &ObjectSourceKind::Procedure),
|
||||
"SELECT m.definition FROM sys.sql_modules m JOIN sys.objects o ON o.object_id = m.object_id JOIN sys.schemas s ON s.schema_id = o.schema_id WHERE s.name = 'dbo' AND o.name = 'refresh_cache' AND o.type IN ('P')"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_postgres_object_source_sql_for_views_and_functions() {
|
||||
assert_eq!(
|
||||
postgres_object_source_sql("public", "active_users", &ObjectSourceKind::View),
|
||||
"SELECT pg_get_viewdef('\"public\".\"active_users\"'::regclass, true)"
|
||||
);
|
||||
assert_eq!(
|
||||
postgres_object_source_sql("public", "recalc_score", &ObjectSourceKind::Function),
|
||||
"SELECT pg_get_functiondef(p.oid) FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'public' AND p.proname = 'recalc_score' AND p.prokind = 'f' ORDER BY p.oid LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_oracle_object_source_sql_using_metadata_api() {
|
||||
assert_eq!(
|
||||
oracle_object_source_sql("HR", "ACTIVE_USERS", &ObjectSourceKind::View),
|
||||
"SELECT DBMS_METADATA.GET_DDL('VIEW', 'ACTIVE_USERS', 'HR') FROM DUAL"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mysql_ddl(pool: &sqlx::mysql::MySqlPool, table: &str) -> Result<String, String> {
|
||||
use sqlx::Row;
|
||||
let sql = format!("SHOW CREATE TABLE `{}`", table.replace('`', "``"));
|
||||
|
||||
@@ -20,6 +20,22 @@ pub struct ObjectInfo {
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ObjectSourceKind {
|
||||
View,
|
||||
Procedure,
|
||||
Function,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ObjectSource {
|
||||
pub name: String,
|
||||
pub object_type: ObjectSourceKind,
|
||||
pub schema: Option<String>,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ColumnInfo {
|
||||
pub name: String,
|
||||
|
||||
@@ -41,6 +41,18 @@ pub async fn list_objects(
|
||||
dbx_core::schema::list_objects_core(&state, &connection_id, &database, &schema).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_object_source(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
schema: String,
|
||||
name: String,
|
||||
object_type: db::ObjectSourceKind,
|
||||
) -> Result<db::ObjectSource, String> {
|
||||
dbx_core::schema::get_object_source_core(&state, &connection_id, &database, &schema, &name, object_type).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_columns(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
|
||||
@@ -83,6 +83,7 @@ pub fn run() {
|
||||
commands::schema::list_databases,
|
||||
commands::schema::list_tables,
|
||||
commands::schema::list_objects,
|
||||
commands::schema::get_object_source,
|
||||
commands::schema::list_schemas,
|
||||
commands::schema::get_columns,
|
||||
commands::schema::list_indexes,
|
||||
|
||||
@@ -87,6 +87,7 @@ async fn main() {
|
||||
.route("/schema/schemas", get(routes::schema::list_schemas))
|
||||
.route("/schema/tables", get(routes::schema::list_tables))
|
||||
.route("/schema/objects", get(routes::schema::list_objects))
|
||||
.route("/schema/object-source", get(routes::schema::get_object_source))
|
||||
.route("/schema/columns", get(routes::schema::list_columns))
|
||||
.route("/schema/indexes", get(routes::schema::list_indexes))
|
||||
.route("/schema/foreign-keys", get(routes::schema::list_foreign_keys))
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct SchemaQuery {
|
||||
pub database: Option<String>,
|
||||
pub schema: Option<String>,
|
||||
pub table: Option<String>,
|
||||
pub object_type: Option<dbx_core::db::ObjectSourceKind>,
|
||||
}
|
||||
|
||||
pub async fn list_databases(
|
||||
@@ -54,6 +55,21 @@ pub async fn list_objects(
|
||||
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
|
||||
}
|
||||
|
||||
pub async fn get_object_source(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
) -> Result<Json<dbx_core::db::ObjectSource>, AppError> {
|
||||
let database = q.database.as_deref().unwrap_or("");
|
||||
let schema = q.schema.as_deref().unwrap_or("");
|
||||
let name = q.table.as_deref().unwrap_or("");
|
||||
let object_type = q.object_type.ok_or_else(|| AppError("Missing object_type".to_string()))?;
|
||||
let result =
|
||||
dbx_core::schema::get_object_source_core(&state.app, &q.connection_id, database, schema, name, object_type)
|
||||
.await
|
||||
.map_err(AppError)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn list_columns(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Query(q): Query<SchemaQuery>,
|
||||
|
||||
Reference in New Issue
Block a user