fdb2 1.0.26 → 1.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/bin/fdb2.js +18 -2
  2. package/dist/public/.vite/manifest.json +4 -4
  3. package/dist/public/bootstrap.js +6 -5
  4. package/dist/public/explorer.css +513 -503
  5. package/dist/public/explorer.js +343 -218
  6. package/dist/public/favicon.ico +0 -0
  7. package/dist/public/index.css +100 -41
  8. package/dist/public/index.js +490 -263
  9. package/dist/public/layout.css +68 -68
  10. package/dist/public/layout.js +10 -10
  11. package/dist/public/rolldown-runtime.js +8 -1
  12. package/dist/public/vconsole.min.js +0 -26
  13. package/dist/public/vue.js +5238 -5192
  14. package/dist/server/cli/format.d.ts +6 -0
  15. package/dist/server/cli/format.d.ts.map +1 -0
  16. package/dist/server/cli/format.js +116 -0
  17. package/dist/server/cli/format.js.map +1 -0
  18. package/dist/server/cli/format.ts +104 -0
  19. package/dist/server/cli/main.d.ts +3 -0
  20. package/dist/server/cli/main.d.ts.map +1 -0
  21. package/dist/server/cli/main.js +710 -0
  22. package/dist/server/cli/main.js.map +1 -0
  23. package/dist/server/cli/main.ts +746 -0
  24. package/dist/server/cli/parse.d.ts +18 -0
  25. package/dist/server/cli/parse.d.ts.map +1 -0
  26. package/dist/server/cli/parse.js +150 -0
  27. package/dist/server/cli/parse.js.map +1 -0
  28. package/dist/server/cli/parse.ts +148 -0
  29. package/dist/server/core/column-spec.d.ts +29 -0
  30. package/dist/server/core/column-spec.d.ts.map +1 -0
  31. package/dist/server/core/column-spec.js +148 -0
  32. package/dist/server/core/column-spec.js.map +1 -0
  33. package/dist/server/core/column-spec.ts +147 -0
  34. package/dist/server/core/config.d.ts +34 -0
  35. package/dist/server/core/config.d.ts.map +1 -0
  36. package/dist/server/core/config.js +187 -0
  37. package/dist/server/core/config.js.map +1 -0
  38. package/dist/server/core/config.ts +165 -0
  39. package/dist/server/core/db-api.d.ts +169 -0
  40. package/dist/server/core/db-api.d.ts.map +1 -0
  41. package/dist/server/core/db-api.js +1078 -0
  42. package/dist/server/core/db-api.js.map +1 -0
  43. package/dist/server/core/db-api.ts +1181 -0
  44. package/dist/server/core/dialect.d.ts +44 -0
  45. package/dist/server/core/dialect.d.ts.map +1 -0
  46. package/dist/server/core/dialect.js +260 -0
  47. package/dist/server/core/dialect.js.map +1 -0
  48. package/dist/server/core/dialect.ts +293 -0
  49. package/dist/server/core/errors.d.ts +22 -0
  50. package/dist/server/core/errors.d.ts.map +1 -0
  51. package/dist/server/core/errors.js +61 -0
  52. package/dist/server/core/errors.js.map +1 -0
  53. package/dist/server/core/errors.ts +58 -0
  54. package/dist/server/core/filter-dsl.d.ts +39 -0
  55. package/dist/server/core/filter-dsl.d.ts.map +1 -0
  56. package/dist/server/core/filter-dsl.js +167 -0
  57. package/dist/server/core/filter-dsl.js.map +1 -0
  58. package/dist/server/core/filter-dsl.ts +180 -0
  59. package/dist/server/core/guard.d.ts +54 -0
  60. package/dist/server/core/guard.d.ts.map +1 -0
  61. package/dist/server/core/guard.js +207 -0
  62. package/dist/server/core/guard.js.map +1 -0
  63. package/dist/server/core/guard.ts +205 -0
  64. package/dist/server/core/query-builder.d.ts +72 -0
  65. package/dist/server/core/query-builder.d.ts.map +1 -0
  66. package/dist/server/core/query-builder.js +478 -0
  67. package/dist/server/core/query-builder.js.map +1 -0
  68. package/dist/server/core/query-builder.ts +502 -0
  69. package/dist/server/service/connection.service.d.ts.map +1 -1
  70. package/dist/server/service/connection.service.js +7 -3
  71. package/dist/server/service/connection.service.js.map +1 -1
  72. package/dist/server/service/connection.service.ts +5 -3
  73. package/dist/server/utils/data-dir.d.ts.map +1 -1
  74. package/dist/server/utils/data-dir.js +2 -0
  75. package/dist/server/utils/data-dir.js.map +1 -1
  76. package/dist/server/utils/data-dir.ts +2 -0
  77. package/electron/updater.js +12 -66
  78. package/package.json +22 -22
  79. package/public/favicon.ico +0 -0
  80. package/public/favicon.png +0 -0
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.computeToken = computeToken;
37
+ exports.verifyToken = verifyToken;
38
+ exports.isTokenUsed = isTokenUsed;
39
+ exports.markTokenUsed = markTokenUsed;
40
+ exports.checkReadonly = checkReadonly;
41
+ exports.confirmThreshold = confirmThreshold;
42
+ exports.checkThreshold = checkThreshold;
43
+ exports.gateWrite = gateWrite;
44
+ exports.audit = audit;
45
+ /**
46
+ * 写操作护栏
47
+ * - 全局只读模式(config.readonly=true 时所有写操作需 --write)
48
+ * - 破坏性操作必须带 --yes
49
+ * - 有条件写操作(update/delete)必须 先 dry-run 拿 confirmToken,再带 token 执行
50
+ * - 影响行数超过阈值需 --force
51
+ * - token 与 op+sql+params 绑定,5 分钟有效(进程无关,跨调用可验证)
52
+ */
53
+ const crypto = __importStar(require("crypto"));
54
+ const fs = __importStar(require("fs"));
55
+ const config_1 = require("./config");
56
+ const errors_1 = require("./errors");
57
+ const data_dir_1 = require("../utils/data-dir");
58
+ const TOKEN_WINDOW_MS = 5 * 60 * 1000;
59
+ function computeToken(op, sql, params) {
60
+ const secret = (0, config_1.getSecret)();
61
+ const bucket = Math.floor(Date.now() / TOKEN_WINDOW_MS);
62
+ const payload = `${op}|${sql}|${JSON.stringify(params)}|${bucket}`;
63
+ return crypto.createHmac('sha256', secret).update(payload).digest('hex').slice(0, 24);
64
+ }
65
+ function verifyToken(token, op, sql, params) {
66
+ if (!token)
67
+ return false;
68
+ const secret = (0, config_1.getSecret)();
69
+ const now = Date.now();
70
+ const bucket = Math.floor(now / TOKEN_WINDOW_MS);
71
+ for (let back = 0; back < 2; back++) {
72
+ const b = bucket - back;
73
+ const payload = `${op}|${sql}|${JSON.stringify(params)}|${b}`;
74
+ const expect = crypto.createHmac('sha256', secret).update(payload).digest('hex').slice(0, 24);
75
+ if (expect === token)
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ // ============ 一次性令牌(防重放) ============
81
+ const USED_TOKENS_FILE = 'used-tokens.json';
82
+ function loadUsedTokens() {
83
+ (0, data_dir_1.ensureDataDir)();
84
+ const p = (0, data_dir_1.getDataPath)(USED_TOKENS_FILE);
85
+ try {
86
+ if (fs.existsSync(p)) {
87
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
88
+ if (raw && typeof raw === 'object')
89
+ return raw;
90
+ }
91
+ }
92
+ catch {
93
+ /* ignore */
94
+ }
95
+ return {};
96
+ }
97
+ function saveUsedTokens(store) {
98
+ const p = (0, data_dir_1.getDataPath)(USED_TOKENS_FILE);
99
+ try {
100
+ fs.writeFileSync(p, JSON.stringify(store), 'utf8');
101
+ if (process.platform !== 'win32')
102
+ fs.chmodSync(p, 0o600);
103
+ }
104
+ catch {
105
+ /* ignore */
106
+ }
107
+ }
108
+ function pruneUsedTokens(store) {
109
+ const now = Date.now();
110
+ const next = {};
111
+ for (const k of Object.keys(store)) {
112
+ if ((store[k] || 0) > now)
113
+ next[k] = store[k];
114
+ }
115
+ return next;
116
+ }
117
+ /** 令牌是否已使用(一次性,防重放) */
118
+ function isTokenUsed(token) {
119
+ const store = loadUsedTokens();
120
+ return !!store[token] && store[token] > Date.now();
121
+ }
122
+ /** 标记令牌已使用,保留到时间窗结束以便过期清理 */
123
+ function markTokenUsed(token) {
124
+ const store = pruneUsedTokens(loadUsedTokens());
125
+ store[token] = Date.now() + TOKEN_WINDOW_MS;
126
+ saveUsedTokens(store);
127
+ }
128
+ /** 检查全局只读模式 */
129
+ function checkReadonly(write) {
130
+ const cfg = (0, config_1.getConfig)();
131
+ if (cfg.readonly && !write) {
132
+ throw new errors_1.AppError('WRITE_BLOCKED', '当前为只读模式,写操作被禁止', {
133
+ hint: '如需临时允许,请加 --write 或执行 fdb2 config set readonly false'
134
+ });
135
+ }
136
+ }
137
+ /** 影响行数阈值 */
138
+ function confirmThreshold() {
139
+ return (0, config_1.getConfig)().confirmThreshold ?? 100;
140
+ }
141
+ /** 检查行数是否超阈值(超阈值需 --force) */
142
+ function checkThreshold(rows, force) {
143
+ if (rows > confirmThreshold() && !force) {
144
+ throw new errors_1.AppError('FORCE_REQUIRED', `操作将影响 ${rows} 行,超过阈值 ${confirmThreshold()}`, {
145
+ hint: '确认无误后请附加 --force 重新 dry-run/执行'
146
+ });
147
+ }
148
+ }
149
+ /**
150
+ * 写操作统一入口:
151
+ * - dryRun=true:估算行数,返回 token(不执行)
152
+ * - 有 confirm:校验 token 并放行
153
+ * - 破坏性操作(destructive=true)还需 --yes
154
+ */
155
+ function gateWrite(opts) {
156
+ checkReadonly(opts.write);
157
+ const { op, sql, params } = opts;
158
+ const destructive = !!opts.destructive;
159
+ if (opts.dryRun) {
160
+ // 预演:校验阈值但不执行
161
+ if (opts.estimatedRows != null) {
162
+ checkThreshold(opts.estimatedRows, opts.force);
163
+ }
164
+ const token = computeToken(op, sql, params);
165
+ return { mode: 'dryrun', token };
166
+ }
167
+ // 正式执行路径
168
+ if (opts.confirm) {
169
+ if (!verifyToken(opts.confirm, op, sql, params)) {
170
+ throw new errors_1.AppError('CONFIRM_EXPIRED', '确认令牌无效或已过期', {
171
+ hint: '请重新执行 dry-run 获取新令牌'
172
+ });
173
+ }
174
+ // 一次性令牌:用过即作废,防止同一令牌重复执行写操作
175
+ if (isTokenUsed(opts.confirm)) {
176
+ throw new errors_1.AppError('TOKEN_REPLAYED', '确认令牌已被使用', {
177
+ hint: '令牌一次性有效,请重新执行 dry-run 获取新令牌'
178
+ });
179
+ }
180
+ if (destructive && !opts.yes) {
181
+ throw new errors_1.AppError('CONFIRM_REQUIRED', '破坏性操作还需 --yes 二次确认', {
182
+ hint: '请附加 --yes 再次执行'
183
+ });
184
+ }
185
+ if (opts.estimatedRows != null) {
186
+ checkThreshold(opts.estimatedRows, opts.force);
187
+ }
188
+ markTokenUsed(opts.confirm);
189
+ return { mode: 'execute' };
190
+ }
191
+ // 无 dry-run 也无 token
192
+ if (destructive) {
193
+ throw new errors_1.AppError('CONFIRM_REQUIRED', '破坏性操作必须先 dry-run 获取确认令牌,再携带 --confirm <token> 与 --yes 执行', {
194
+ hint: '步骤:1) fdb2 ... --dry-run 2) fdb2 ... --confirm <token> --yes'
195
+ });
196
+ }
197
+ throw new errors_1.AppError('REQUIRE_DRYRUN', '写操作必须先 dry-run 预演', {
198
+ hint: '步骤:1) fdb2 ... --dry-run 2) fdb2 ... --confirm <token>'
199
+ });
200
+ }
201
+ /** 审计一个操作 */
202
+ function audit(opts) {
203
+ // 延迟 require 避免循环依赖
204
+ const { appendAudit } = require('./config');
205
+ appendAudit({ ...opts });
206
+ }
207
+ //# sourceMappingURL=guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../../../server/core/guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * 写操作护栏
3
+ * - 全局只读模式(config.readonly=true 时所有写操作需 --write)
4
+ * - 破坏性操作必须带 --yes
5
+ * - 有条件写操作(update/delete)必须 先 dry-run 拿 confirmToken,再带 token 执行
6
+ * - 影响行数超过阈值需 --force
7
+ * - token 与 op+sql+params 绑定,5 分钟有效(进程无关,跨调用可验证)
8
+ */
9
+ import * as crypto from 'crypto';
10
+ import * as fs from 'fs';
11
+ import { getSecret, getConfig } from './config';
12
+ import { AppError } from './errors';
13
+ import { getDataPath, ensureDataDir } from '../utils/data-dir';
14
+
15
+ export interface WriteOpts {
16
+ dryRun?: boolean;
17
+ confirm?: string | null;
18
+ yes?: boolean;
19
+ force?: boolean;
20
+ write?: boolean;
21
+ }
22
+
23
+ const TOKEN_WINDOW_MS = 5 * 60 * 1000;
24
+
25
+ export function computeToken(op: string, sql: string, params: any[]): string {
26
+ const secret = getSecret();
27
+ const bucket = Math.floor(Date.now() / TOKEN_WINDOW_MS);
28
+ const payload = `${op}|${sql}|${JSON.stringify(params)}|${bucket}`;
29
+ return crypto.createHmac('sha256', secret).update(payload).digest('hex').slice(0, 24);
30
+ }
31
+
32
+ export function verifyToken(token: string, op: string, sql: string, params: any[]): boolean {
33
+ if (!token) return false;
34
+ const secret = getSecret();
35
+ const now = Date.now();
36
+ const bucket = Math.floor(now / TOKEN_WINDOW_MS);
37
+ for (let back = 0; back < 2; back++) {
38
+ const b = bucket - back;
39
+ const payload = `${op}|${sql}|${JSON.stringify(params)}|${b}`;
40
+ const expect = crypto.createHmac('sha256', secret).update(payload).digest('hex').slice(0, 24);
41
+ if (expect === token) return true;
42
+ }
43
+ return false;
44
+ }
45
+
46
+ // ============ 一次性令牌(防重放) ============
47
+ const USED_TOKENS_FILE = 'used-tokens.json';
48
+
49
+ function loadUsedTokens(): Record<string, number> {
50
+ ensureDataDir();
51
+ const p = getDataPath(USED_TOKENS_FILE);
52
+ try {
53
+ if (fs.existsSync(p)) {
54
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
55
+ if (raw && typeof raw === 'object') return raw as Record<string, number>;
56
+ }
57
+ } catch {
58
+ /* ignore */
59
+ }
60
+ return {};
61
+ }
62
+
63
+ function saveUsedTokens(store: Record<string, number>): void {
64
+ const p = getDataPath(USED_TOKENS_FILE);
65
+ try {
66
+ fs.writeFileSync(p, JSON.stringify(store), 'utf8');
67
+ if (process.platform !== 'win32') fs.chmodSync(p, 0o600);
68
+ } catch {
69
+ /* ignore */
70
+ }
71
+ }
72
+
73
+ function pruneUsedTokens(store: Record<string, number>): Record<string, number> {
74
+ const now = Date.now();
75
+ const next: Record<string, number> = {};
76
+ for (const k of Object.keys(store)) {
77
+ if ((store[k] || 0) > now) next[k] = store[k];
78
+ }
79
+ return next;
80
+ }
81
+
82
+ /** 令牌是否已使用(一次性,防重放) */
83
+ export function isTokenUsed(token: string): boolean {
84
+ const store = loadUsedTokens();
85
+ return !!store[token] && store[token] > Date.now();
86
+ }
87
+
88
+ /** 标记令牌已使用,保留到时间窗结束以便过期清理 */
89
+ export function markTokenUsed(token: string): void {
90
+ const store = pruneUsedTokens(loadUsedTokens());
91
+ store[token] = Date.now() + TOKEN_WINDOW_MS;
92
+ saveUsedTokens(store);
93
+ }
94
+
95
+ /** 检查全局只读模式 */
96
+ export function checkReadonly(write: boolean | undefined): void {
97
+ const cfg = getConfig();
98
+ if (cfg.readonly && !write) {
99
+ throw new AppError('WRITE_BLOCKED', '当前为只读模式,写操作被禁止', {
100
+ hint: '如需临时允许,请加 --write 或执行 fdb2 config set readonly false'
101
+ });
102
+ }
103
+ }
104
+
105
+ /** 影响行数阈值 */
106
+ export function confirmThreshold(): number {
107
+ return getConfig().confirmThreshold ?? 100;
108
+ }
109
+
110
+ /** 检查行数是否超阈值(超阈值需 --force) */
111
+ export function checkThreshold(rows: number, force: boolean | undefined): void {
112
+ if (rows > confirmThreshold() && !force) {
113
+ throw new AppError('FORCE_REQUIRED', `操作将影响 ${rows} 行,超过阈值 ${confirmThreshold()}`, {
114
+ hint: '确认无误后请附加 --force 重新 dry-run/执行'
115
+ });
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 写操作统一入口:
121
+ * - dryRun=true:估算行数,返回 token(不执行)
122
+ * - 有 confirm:校验 token 并放行
123
+ * - 破坏性操作(destructive=true)还需 --yes
124
+ */
125
+ export function gateWrite(
126
+ opts: {
127
+ op: string;
128
+ sql: string;
129
+ params: any[];
130
+ estimatedRows?: number | null;
131
+ destructive?: boolean;
132
+ write?: boolean;
133
+ dryRun?: boolean;
134
+ confirm?: string | null;
135
+ yes?: boolean;
136
+ force?: boolean;
137
+ }
138
+ ): { mode: 'execute' | 'dryrun'; token?: string } {
139
+ checkReadonly(opts.write);
140
+
141
+ const { op, sql, params } = opts;
142
+ const destructive = !!opts.destructive;
143
+
144
+ if (opts.dryRun) {
145
+ // 预演:校验阈值但不执行
146
+ if (opts.estimatedRows != null) {
147
+ checkThreshold(opts.estimatedRows, opts.force);
148
+ }
149
+ const token = computeToken(op, sql, params);
150
+ return { mode: 'dryrun', token };
151
+ }
152
+
153
+ // 正式执行路径
154
+ if (opts.confirm) {
155
+ if (!verifyToken(opts.confirm, op, sql, params)) {
156
+ throw new AppError('CONFIRM_EXPIRED', '确认令牌无效或已过期', {
157
+ hint: '请重新执行 dry-run 获取新令牌'
158
+ });
159
+ }
160
+ // 一次性令牌:用过即作废,防止同一令牌重复执行写操作
161
+ if (isTokenUsed(opts.confirm)) {
162
+ throw new AppError('TOKEN_REPLAYED', '确认令牌已被使用', {
163
+ hint: '令牌一次性有效,请重新执行 dry-run 获取新令牌'
164
+ });
165
+ }
166
+ if (destructive && !opts.yes) {
167
+ throw new AppError('CONFIRM_REQUIRED', '破坏性操作还需 --yes 二次确认', {
168
+ hint: '请附加 --yes 再次执行'
169
+ });
170
+ }
171
+ if (opts.estimatedRows != null) {
172
+ checkThreshold(opts.estimatedRows, opts.force);
173
+ }
174
+ markTokenUsed(opts.confirm);
175
+ return { mode: 'execute' };
176
+ }
177
+
178
+ // 无 dry-run 也无 token
179
+ if (destructive) {
180
+ throw new AppError('CONFIRM_REQUIRED', '破坏性操作必须先 dry-run 获取确认令牌,再携带 --confirm <token> 与 --yes 执行', {
181
+ hint: '步骤:1) fdb2 ... --dry-run 2) fdb2 ... --confirm <token> --yes'
182
+ });
183
+ }
184
+ throw new AppError('REQUIRE_DRYRUN', '写操作必须先 dry-run 预演', {
185
+ hint: '步骤:1) fdb2 ... --dry-run 2) fdb2 ... --confirm <token>'
186
+ });
187
+ }
188
+
189
+ /** 审计一个操作 */
190
+ export function audit(opts: {
191
+ action: string;
192
+ conn?: string | null;
193
+ database?: string | null;
194
+ table?: string | null;
195
+ kind?: string | null;
196
+ sql?: string | null;
197
+ params?: any;
198
+ rows?: number | null;
199
+ ok: boolean;
200
+ error?: string | null;
201
+ }): void {
202
+ // 延迟 require 避免循环依赖
203
+ const { appendAudit } = require('./config') as typeof import('./config');
204
+ appendAudit({ ...opts } as any);
205
+ }
@@ -0,0 +1,72 @@
1
+ import { FilterNode } from './filter-dsl';
2
+ export interface ColumnMeta {
3
+ name: string;
4
+ type?: string;
5
+ isAutoIncrement?: boolean;
6
+ isPrimary?: boolean;
7
+ nullable?: boolean;
8
+ }
9
+ export interface CompiledQuery {
10
+ sql: string;
11
+ params: any[];
12
+ }
13
+ /** 内联模式下的字面量(oracle/mssql/sap 使用) */
14
+ export declare function renderLiteral(dbType: string, value: any): string;
15
+ export declare class SqlBuilder {
16
+ private dbType;
17
+ private quote;
18
+ private cols;
19
+ constructor(dbType: string, columns: ColumnMeta[]);
20
+ get hasColumns(): boolean;
21
+ /** 白名单校验 + 候选列名提示 */
22
+ checkField(field: string): void;
23
+ /** 编译过滤树 */
24
+ where(node: FilterNode | null): CompiledQuery;
25
+ private node;
26
+ private leaf;
27
+ /** 按列类型归一化值:int/decimal 列收到数字字符串时转 number,boolean 列归一 */
28
+ normValue(field: string, v: any): any;
29
+ /** '%P%' → 实际占位符(? 或 $n),并收集参数 */
30
+ private collapse;
31
+ private pick;
32
+ /** 将收集到的 param 数组按出现顺序填充占位符 */
33
+ static finalize(dbType: string, sql: string, params: any[]): CompiledQuery;
34
+ }
35
+ /** 由多条语句片段合成最终 SQL */
36
+ export declare function compose(dbType: string, statement: {
37
+ sql: string;
38
+ params: any[];
39
+ }): CompiledQuery;
40
+ export declare function buildSelect(dbType: string, table: string, columns: ColumnMeta[], opts: {
41
+ select?: string[];
42
+ where?: FilterNode | null;
43
+ sorts?: Array<{
44
+ field: string;
45
+ dir: 'asc' | 'desc';
46
+ }>;
47
+ limit?: number | null;
48
+ offset?: number;
49
+ countOnly?: boolean;
50
+ }): CompiledQuery;
51
+ export declare function buildCount(dbType: string, table: string, columns: ColumnMeta[], where?: FilterNode | null): CompiledQuery;
52
+ export declare function buildInsert(dbType: string, table: string, columns: ColumnMeta[], data: Record<string, any>, opts?: {
53
+ skipAutoIncrement?: boolean;
54
+ }): CompiledQuery;
55
+ export declare function buildUpdate(dbType: string, table: string, columns: ColumnMeta[], setData: Record<string, any>, where?: FilterNode | null): CompiledQuery;
56
+ export declare function buildDelete(dbType: string, table: string, columns: ColumnMeta[], where?: FilterNode | null): CompiledQuery;
57
+ /** 排序参数解析:field:asc|desc(可重复) */
58
+ export declare function parseSorts(list: string[]): Array<{
59
+ field: string;
60
+ dir: 'asc' | 'desc';
61
+ }>;
62
+ /** Levenshtein 最短距离 */
63
+ export declare function levenshtein(a: string, b: string): number;
64
+ export declare function suggestField(input: string, candidates: string[]): string | null;
65
+ /** SQL 语句类型分类(逃生舱防护) */
66
+ export declare function classifySql(sql: string): {
67
+ kind: 'select' | 'write' | 'ddl' | 'multi' | 'other';
68
+ statements: string[];
69
+ hasWrite: boolean;
70
+ destructive: boolean;
71
+ };
72
+ //# sourceMappingURL=query-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-builder.d.ts","sourceRoot":"","sources":["../../../server/core/query-builder.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,GAAG,EAAE,CAAC;CACf;AAUD,qCAAqC;AACrC,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,MAAM,CAahE;AAoBD,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,IAAI,CAA0B;gBAE1B,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE;IAOjD,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,qBAAqB;IACrB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAW/B,YAAY;IACZ,KAAK,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,aAAa;IAM7C,OAAO,CAAC,IAAI;IAsBZ,OAAO,CAAC,IAAI;IAqEZ,yDAAyD;IACzD,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,GAAG,GAAG;IAuBrC,kCAAkC;IAClC,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,IAAI;IAMZ,+BAA+B;IAC/B,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,aAAa;CAgB3E;AAED,sBAAsB;AACtB,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,GAAG,EAAE,CAAA;CAAE,GAAG,aAAa,CAEhG;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,EAAE,EACrB,IAAI,EAAE;IACJ,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GACA,aAAa,CAuDf;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,aAAa,CAEzH;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,EAAE,EACrB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,IAAI,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GACrC,aAAa,CA4Bf;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI,GACxB,aAAa,CAiCf;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,EAAE,EACrB,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI,GACxB,aAAa,CASf;AAED,iCAAiC;AACjC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,CAAC,CAUxF;AAED,uBAAuB;AACvB,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAexD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAa/E;AAED,wBAAwB;AACxB,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG;IACxC,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,CAAC;IACrD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACtB,CAeA"}