整理代码
This commit is contained in:
1 parent
e87c4ee6a8
commit
a45e33f94b
39 files changed
+264
-917
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
@@ -297,11 +297,6 @@ export async function query(sql, database, tabId) {
|
||||
}
|
||||
}
|
||||
|
||||
// 灌演示数据。
|
||||
export async function seedDemoData(database) {
|
||||
return request('/api/demo-data', jsonInit('POST', {}), database);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 设置
|
||||
|
||||
// 读取服务器设置。
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
Reference in new issue
Block a user