整理代码

This commit is contained in:
LIm1tZz7 committed 2026-10-13 14:09:36 +08:00
1 parent e87c4ee6a8
commit a45e33f94b
39 files changed
+264 -917

No files matched your search

-34
View File
@@ -17,10 +17,8 @@ import ChuSQL.Interface.Config (
)
import ChuSQL.Interface.Link (clientPing, closeClient, connectClient)
import ChuSQL.Interface.RateLimit (newRateLimiter)
import ChuSQL.Interface.Session (authenticateSession, newSession)
import ChuSQL.Interface.Settings (effectiveSettings)
import ChuSQL.Interface.TOML (resolveConfigPath)
import ChuSQL.Web.Demo (SeedReport (..), seedDemo)
import Control.Exception (IOException, try)
import Data.Char (toLower)
import Data.String (fromString)
@@ -52,7 +50,6 @@ data Options = Options
, optMaxPageSize :: Maybe Int
, optMaxRows :: Maybe Int
, optMaxSqlLength :: Maybe Int
, optSeedDemo :: Maybe Bool
, optHelp :: Bool
}
@@ -74,7 +71,6 @@ emptyOptions =
, optMaxPageSize = Nothing
, optMaxRows = Nothing
, optMaxSqlLength = Nothing
, optSeedDemo = Nothing
, optHelp = False
}
@@ -99,7 +95,6 @@ parseArgs = go emptyOptions
go opts ("--max-page-size" : v : rest) = withInt "--max-page-size" v (\n -> go opts{optMaxPageSize = Just n} rest)
go opts ("--max-rows" : v : rest) = withInt "--max-rows" v (\n -> go opts{optMaxRows = Just n} rest)
go opts ("--max-sql-length" : v : rest) = withInt "--max-sql-length" v (\n -> go opts{optMaxSqlLength = Just n} rest)
go opts ("--seed" : v : rest) = go opts{optSeedDemo = Just (isTrue v)} rest
go opts ("--cookie-secure" : v : rest) = go opts{optCookieSecure = Just (isTrue v)} rest
go _ (a : _) = Left ("unrecognized argument or missing value: " ++ a)
@@ -136,7 +131,6 @@ usage = do
putStrLn " --max-page-size N rows per page ceiling ([web] max_page_size, default 500)"
putStrLn " --max-rows N rows returned per query ([web] max_rows, default 1000)"
putStrLn " --max-sql-length N max SQL characters ([web] max_sql_length, default 20000)"
putStrLn " --seed BOOL load demo data when the db is empty ([web] seed, default false)"
putStrLn ""
putStrLn "administrator password: lives in the __system_users system table, never in settings.toml"
putStrLn " the installer always sets one; an account whose hash is still empty signs in with an empty password"
@@ -164,7 +158,6 @@ applyOptions cfg opts =
, wcMaxPageSize = orElse (optMaxPageSize opts) (wcMaxPageSize cfg)
, wcMaxRows = orElse (optMaxRows opts) (wcMaxRows cfg)
, wcMaxSqlLength = orElse (optMaxSqlLength opts) (wcMaxSqlLength cfg)
, wcSeedDemo = orElse (optSeedDemo opts) (wcSeedDemo cfg)
}
where
-- | 有就用新的,没有留旧值
@@ -220,7 +213,6 @@ run opts = do
printBanner cfg configPath staticDir
reportCredential cfg legacy
checkServer (aeServerHost env) (aeServerPort env)
seedIfWanted cfg (aeServerHost env) (aeServerPort env)
let settings =
Warp.setPort (wcPort cfg) (Warp.setHost (fromString (wcHost cfg)) Warp.defaultSettings)
serve settings app
@@ -262,32 +254,6 @@ reportCredential cfg legacy = do
putStrLn " !! remove it: the administrator password lives in the __system_users system table"
else pure ()
-- | 配了 seed 就补演示数据(管理员已设口令时跳过)
seedIfWanted :: WebConfig -> Text -> Int -> IO ()
seedIfWanted cfg host port
| not (wcSeedDemo cfg) = pure ()
| otherwise = do
opened <- connectClient host port
case opened of
Left message -> TIO.putStrLn ("demo data skipped: " <> message)
Right client -> do
session <- newSession client
signed <- authenticateSession session (rootUserName cfg) ""
case signed of
Left message -> TIO.putStrLn ("demo data skipped: " <> message <> " (set the administrator password after seeding)")
Right () -> do
result <- seedDemo session
case result of
Left message -> TIO.putStrLn ("demo data skipped: " <> message)
Right report ->
TIO.putStrLn
( "demo data: "
<> (if null (srCreated report) then "already present" else "created " <> T.pack (unwords (srCreated report)))
<> (if null (srSkipped report) then "" else ", kept " <> T.pack (unwords (srSkipped report)))
<> (if null (srIndexes report) then "" else ", index on " <> T.pack (unwords (srIndexes report)))
)
closeClient client
-- | ping 一次 server,报告它通不通
checkServer :: Text -> Int -> IO ()
checkServer host port = do
-1
View File
@@ -12,7 +12,6 @@ build-type: Simple
library
hs-source-dirs: src
exposed-modules: ChuSQL.Web.API
ChuSQL.Web.Demo
ChuSQL.Web.Secure
ChuSQL.Web.Static
ChuSQL.Web.UISettings
-20
View File
@@ -101,7 +101,6 @@ import ChuSQL.Interface.Settings (
writeSettingsFile,
)
import ChuSQL.Interface.TOML (defaultConfigFile)
import ChuSQL.Web.Demo (SeedReport (..), seedDemo)
import ChuSQL.Web.Secure (bodyLimitDynamic, sameOriginOnly, securityHeaders, contentSecurityPolicy)
import ChuSQL.Web.Static (contentTypeOf, readStatic, safeRelative)
import ChuSQL.Web.UISettings (
@@ -331,7 +330,6 @@ routes env = do
post "/api/tables/:t/indexes" (withDatabase env createIndexH)
delete "/api/tables/:t/indexes/:col" (withDatabase env dropIndexH)
delete "/api/tables/:t/columns/:col" (withDatabase env dropColumnH)
post "/api/demo-data" (withDatabase env demoDataH)
get "/api/roles" (withDatabase env rolesH)
post "/api/roles" (withDatabase env createRoleH)
delete "/api/roles/:name" (withDatabase env dropRoleH)
@@ -1241,24 +1239,6 @@ dropColumnH env = do
Left err -> reply400 "bad_request" (T.pack err)
Right sql -> runWrite ws sql
-- | 一键灌演示数据(缺什么补什么,可以反复点)
demoDataH :: AppEnv -> ActionM ()
demoDataH env = do
ws <- requireAdminSession env
_ <- requireDatabaseName ws
result <- liftIO (seedDemo (wsSession ws))
case result of
Left message -> serverError message
Right report ->
json
( object
[ "ok" .= True
, "created" .= srCreated report
, "skipped" .= srSkipped report
, "indexes" .= srIndexes report
]
)
-- | 角色总览:只有管理员能看
rolesH :: AppEnv -> ActionM ()
rolesH env = do
-166
View File
@@ -1,166 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module ChuSQL.Web.Demo (
SeedReport (..),
demoTableSpecs,
demoRows,
demoIndexes,
demoNames,
demoPlan,
seedDemo,
) where
import ChuSQL.Core.Model (Value (..))
import ChuSQL.Core.Protocol (TableInfo (..))
import ChuSQL.Interface.Actions (
ColumnSpec (..),
CreateTableSpec (..),
createIndexSql,
createTableSql,
insertRowsSql,
)
import ChuSQL.Interface.Session (Session, catalog, runStatement)
import Data.Text (Text)
import qualified Data.Text as T
-- 演示数据:三张表加一个二级索引,语句与界面走同一套拼 SQL 的代码。
-- | 演示表(建表顺序即依赖顺序;没有外键约束,纯展示)
demoTableSpecs :: [CreateTableSpec]
demoTableSpecs =
[ CreateTableSpec "users" [ColumnSpec "id" "int", ColumnSpec "name" "str", ColumnSpec "age" "int"]
, CreateTableSpec "orders" [ColumnSpec "id" "int", ColumnSpec "user_id" "int", ColumnSpec "product" "str", ColumnSpec "amount" "int"]
, CreateTableSpec "products" [ColumnSpec "id" "int", ColumnSpec "name" "str", ColumnSpec "sku" "int", ColumnSpec "price" "int"]
]
-- | 每张表的演示行
demoRows :: [(T.Text, [[(String, Value)]])]
demoRows =
[ ( "users"
, [ [("id", VInt i), ("name", VStr name), ("age", VInt age)]
| (i, (name, age)) <- zip [1 :: Int ..] userRecords
]
)
, ( "orders"
, [ [("id", VInt i), ("user_id", VInt uid), ("product", VStr p), ("amount", VInt a)]
| (i, (uid, p, a)) <- zip [1 :: Int ..] orderRecords
]
)
, ( "products"
, [ [("id", VInt i), ("name", VStr n), ("sku", VInt sku), ("price", VInt price)]
| (i, (n, sku, price)) <- zip [1 :: Int ..] productRecords
]
)
]
-- | 演示索引:products.sku 取值唯一
demoIndexes :: [(T.Text, T.Text)]
demoIndexes = [("products", "sku")]
-- | 演示数据涉及的表名
demoNames :: [T.Text]
demoNames = map ctsTable demoTableSpecs
-- | 完整的一套语句(建表 + 插行 + 建索引),按顺序执行
demoPlan :: Either String [String]
demoPlan = do
creates <- mapM createTableSql demoTableSpecs
inserts <- mapM (uncurry insertRowsSql) demoRows
indexes <- mapM (uncurry createIndexSql) demoIndexes
pure (creates ++ inserts ++ indexes)
data SeedReport = SeedReport
{ srCreated :: [String]
, srSkipped :: [String]
, srIndexes :: [String]
}
deriving (Show, Eq)
-- | 按需灌演示数据:缺表补表、缺索引补索引
seedDemo :: Session -> IO (Either Text SeedReport)
seedDemo session = do
catalogResult <- catalog session
case catalogResult of
Left message -> pure (Left message)
Right infos -> do
let existingTables = map tiTable infos
missing = [spec | spec <- demoTableSpecs, T.unpack (ctsTable spec) `notElem` existingTables]
skipped = [T.unpack (ctsTable spec) | spec <- demoTableSpecs, T.unpack (ctsTable spec) `elem` existingTables]
missingNames = map ctsTable missing
wantedIndexes =
[ (t, c)
| (t, c) <- demoIndexes
, t `elem` missingNames || indexMissing infos t c
]
created <- runAll (map createTableSql missing)
case created of
Left message -> pure (Left message)
Right () -> do
rowsDone <- runAll [insertRowsSql t rs | (t, rs) <- demoRows, t `elem` missingNames]
case rowsDone of
Left message -> pure (Left message)
Right () -> do
indexes <- runAll [createIndexSql t c | (t, c) <- wantedIndexes]
pure $ fmap (const (SeedReport (map T.unpack missingNames) skipped (map (T.unpack . snd) wantedIndexes))) indexes
where
-- | 顺序跑完一串语句,首错即停
runAll :: [Either String String] -> IO (Either Text ())
runAll [] = pure (Right ())
runAll (sql : rest) = case sql of
Left e -> pure (Left (T.pack e))
Right statement -> do
result <- runStatement session (T.pack statement)
case result of
Left message -> pure (Left message)
Right _ -> runAll rest
-- | 目录里这张表缺不缺这个索引
indexMissing :: [TableInfo] -> Text -> Text -> Bool
indexMissing infos t c = case lookup (T.unpack t) [(tiTable i, tiIndexes i) | i <- infos] of
Nothing -> True
Just cols -> T.unpack c `notElem` cols
-- | 演示用户数据
userRecords :: [(String, Int)]
userRecords =
[ ("Alice", 24)
, ("Bob", 31)
, ("Carol", 45)
, ("Dave", 19)
, ("Erin", 38)
, ("Frank", 52)
, ("Grace", 27)
, ("Heidi", 33)
, ("Ivan", 41)
, ("Judy", 29)
]
-- | 演示订单数据
orderRecords :: [(Int, String, Int)]
orderRecords =
[ (1, "Laptop", 1299)
, (1, "Mouse", 25)
, (2, "Keyboard", 79)
, (3, "Monitor", 349)
, (3, "Webcam", 89)
, (4, "Headset", 119)
, (5, "SSD", 139)
, (5, "RAM", 99)
, (6, "Dock", 199)
, (7, "Cable", 12)
, (8, "Stand", 45)
, (9, "Lamp", 39)
]
-- | 演示产品数据
productRecords :: [(String, Int, Int)]
productRecords =
[ ("Laptop", 1001, 1299)
, ("Mouse", 1002, 25)
, ("Keyboard", 1003, 79)
, ("Monitor", 1004, 349)
, ("Webcam", 1005, 89)
, ("Headset", 1006, 119)
, ("SSD", 1007, 139)
, ("RAM", 1008, 99)
]
-5
View File
@@ -297,11 +297,6 @@ export async function query(sql, database, tabId) {
}
}
// 灌演示数据。
export async function seedDemoData(database) {
return request('/api/demo-data', jsonInit('POST', {}), database);
}
// ---------------------------------------------------------------- 设置
// 读取服务器设置。
+1 -14
View File
@@ -147,7 +147,6 @@ function renderShell() {
<div class="brand"><span class="brand-mark">ChuSQL</span><span class="brand-name">数据库终端</span></div>
<nav class="titlebar-actions">
<button data-action="new-query" title="新建查询">新建查询</button>
<button data-action="demo-data" title="灌一份演示数据">演示数据</button>
<button data-action="refresh" title="重新加载当前库">刷新</button>
<button data-action="settings" title="IDE 与服务器设置">设置</button>
</nav>
@@ -1400,7 +1399,7 @@ function startSplitterDrag(kind, event) {
// 这些动作都要求已经选中一个库(服务没有默认库)
const DATABASE_ACTIONS = new Set([
'open-normalization', 'analyze-normalization', 'open-table', 'demo-data', 'open-table-dialog', 'create-table',
'open-normalization', 'analyze-normalization', 'open-table', 'open-table-dialog', 'create-table',
'open-index-dialog', 'create-index', 'drop-table', 'drop-column', 'drop-index',
'drop-database', 'commit', 'rollback', 'copy-sql',
]);
@@ -1496,18 +1495,6 @@ function onClick(event) {
case 'refresh':
void loadWorkspace().then(reloadOpenTables);
return;
case 'demo-data':
void (async () => {
try {
const report = await api.seedDemoData(state.database);
log('info', `演示数据:新建 ${(report.created ?? []).length} 张,保留 ${(report.skipped ?? []).length} 张。`);
await loadWorkspace();
await reloadOpenTables();
} catch (error) {
fail(error);
}
})();
return;
case 'open-db-dialog':
region('dlg-db-name').value = '';
region('dlg-db-error').textContent = '';