@berthojoris/mcp-mysql-server 1.5.0 → 1.6.0
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.
- package/DOCUMENTATIONS.md +477 -17
- package/README.md +102 -5
- package/dist/index.d.ts +401 -0
- package/dist/index.js +311 -0
- package/dist/mcp-server.js +1067 -0
- package/dist/tools/constraintTools.d.ts +108 -0
- package/dist/tools/constraintTools.js +405 -0
- package/dist/tools/functionTools.d.ts +93 -0
- package/dist/tools/functionTools.js +351 -0
- package/dist/tools/indexTools.d.ts +81 -0
- package/dist/tools/indexTools.js +345 -0
- package/dist/tools/maintenanceTools.d.ts +111 -0
- package/dist/tools/maintenanceTools.js +371 -0
- package/dist/tools/processTools.d.ts +106 -0
- package/dist/tools/processTools.js +305 -0
- package/dist/tools/triggerTools.d.ts +76 -0
- package/dist/tools/triggerTools.js +294 -0
- package/dist/tools/viewTools.d.ts +91 -0
- package/dist/tools/viewTools.js +330 -0
- package/package.json +1 -1
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MaintenanceTools = void 0;
|
|
7
|
+
const connection_1 = __importDefault(require("../db/connection"));
|
|
8
|
+
const config_1 = require("../config/config");
|
|
9
|
+
class MaintenanceTools {
|
|
10
|
+
constructor(security) {
|
|
11
|
+
this.db = connection_1.default.getInstance();
|
|
12
|
+
this.security = security;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate database access - ensures only the connected database can be accessed
|
|
16
|
+
*/
|
|
17
|
+
validateDatabaseAccess(requestedDatabase) {
|
|
18
|
+
const connectedDatabase = config_1.dbConfig.database;
|
|
19
|
+
if (!connectedDatabase) {
|
|
20
|
+
return {
|
|
21
|
+
valid: false,
|
|
22
|
+
database: '',
|
|
23
|
+
error: 'No database specified in connection string. Cannot access any database.'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (!requestedDatabase) {
|
|
27
|
+
return {
|
|
28
|
+
valid: true,
|
|
29
|
+
database: connectedDatabase
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (requestedDatabase !== connectedDatabase) {
|
|
33
|
+
return {
|
|
34
|
+
valid: false,
|
|
35
|
+
database: '',
|
|
36
|
+
error: `Access denied. You can only access the connected database '${connectedDatabase}'. Requested database '${requestedDatabase}' is not allowed.`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
valid: true,
|
|
41
|
+
database: connectedDatabase
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Analyze table to update index statistics
|
|
46
|
+
*/
|
|
47
|
+
async analyzeTable(params) {
|
|
48
|
+
try {
|
|
49
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
50
|
+
if (!dbValidation.valid) {
|
|
51
|
+
return { status: 'error', error: dbValidation.error };
|
|
52
|
+
}
|
|
53
|
+
const { table_name } = params;
|
|
54
|
+
const database = dbValidation.database;
|
|
55
|
+
// Validate table name
|
|
56
|
+
if (!this.security.validateIdentifier(table_name).valid) {
|
|
57
|
+
return { status: 'error', error: 'Invalid table name' };
|
|
58
|
+
}
|
|
59
|
+
const query = `ANALYZE TABLE \`${database}\`.\`${table_name}\``;
|
|
60
|
+
const results = await this.db.query(query);
|
|
61
|
+
return {
|
|
62
|
+
status: 'success',
|
|
63
|
+
data: results[0],
|
|
64
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
status: 'error',
|
|
70
|
+
error: error.message,
|
|
71
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Optimize table to reclaim unused space and defragment
|
|
77
|
+
*/
|
|
78
|
+
async optimizeTable(params) {
|
|
79
|
+
try {
|
|
80
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
81
|
+
if (!dbValidation.valid) {
|
|
82
|
+
return { status: 'error', error: dbValidation.error };
|
|
83
|
+
}
|
|
84
|
+
const { table_name } = params;
|
|
85
|
+
const database = dbValidation.database;
|
|
86
|
+
// Validate table name
|
|
87
|
+
if (!this.security.validateIdentifier(table_name).valid) {
|
|
88
|
+
return { status: 'error', error: 'Invalid table name' };
|
|
89
|
+
}
|
|
90
|
+
const query = `OPTIMIZE TABLE \`${database}\`.\`${table_name}\``;
|
|
91
|
+
const results = await this.db.query(query);
|
|
92
|
+
return {
|
|
93
|
+
status: 'success',
|
|
94
|
+
data: results[0],
|
|
95
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
status: 'error',
|
|
101
|
+
error: error.message,
|
|
102
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Check table for errors
|
|
108
|
+
*/
|
|
109
|
+
async checkTable(params) {
|
|
110
|
+
try {
|
|
111
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
112
|
+
if (!dbValidation.valid) {
|
|
113
|
+
return { status: 'error', error: dbValidation.error };
|
|
114
|
+
}
|
|
115
|
+
const { table_name, check_type } = params;
|
|
116
|
+
const database = dbValidation.database;
|
|
117
|
+
// Validate table name
|
|
118
|
+
if (!this.security.validateIdentifier(table_name).valid) {
|
|
119
|
+
return { status: 'error', error: 'Invalid table name' };
|
|
120
|
+
}
|
|
121
|
+
let query = `CHECK TABLE \`${database}\`.\`${table_name}\``;
|
|
122
|
+
if (check_type) {
|
|
123
|
+
query += ` ${check_type}`;
|
|
124
|
+
}
|
|
125
|
+
const results = await this.db.query(query);
|
|
126
|
+
return {
|
|
127
|
+
status: 'success',
|
|
128
|
+
data: results[0],
|
|
129
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return {
|
|
134
|
+
status: 'error',
|
|
135
|
+
error: error.message,
|
|
136
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Repair table (MyISAM, ARCHIVE, CSV only)
|
|
142
|
+
*/
|
|
143
|
+
async repairTable(params) {
|
|
144
|
+
try {
|
|
145
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
146
|
+
if (!dbValidation.valid) {
|
|
147
|
+
return { status: 'error', error: dbValidation.error };
|
|
148
|
+
}
|
|
149
|
+
const { table_name, quick = false, extended = false, use_frm = false } = params;
|
|
150
|
+
const database = dbValidation.database;
|
|
151
|
+
// Validate table name
|
|
152
|
+
if (!this.security.validateIdentifier(table_name).valid) {
|
|
153
|
+
return { status: 'error', error: 'Invalid table name' };
|
|
154
|
+
}
|
|
155
|
+
let query = `REPAIR TABLE \`${database}\`.\`${table_name}\``;
|
|
156
|
+
const options = [];
|
|
157
|
+
if (quick)
|
|
158
|
+
options.push('QUICK');
|
|
159
|
+
if (extended)
|
|
160
|
+
options.push('EXTENDED');
|
|
161
|
+
if (use_frm)
|
|
162
|
+
options.push('USE_FRM');
|
|
163
|
+
if (options.length > 0) {
|
|
164
|
+
query += ` ${options.join(' ')}`;
|
|
165
|
+
}
|
|
166
|
+
const results = await this.db.query(query);
|
|
167
|
+
return {
|
|
168
|
+
status: 'success',
|
|
169
|
+
data: results[0],
|
|
170
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
return {
|
|
175
|
+
status: 'error',
|
|
176
|
+
error: error.message,
|
|
177
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Truncate table (remove all rows quickly)
|
|
183
|
+
*/
|
|
184
|
+
async truncateTable(params) {
|
|
185
|
+
try {
|
|
186
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
187
|
+
if (!dbValidation.valid) {
|
|
188
|
+
return { status: 'error', error: dbValidation.error };
|
|
189
|
+
}
|
|
190
|
+
const { table_name } = params;
|
|
191
|
+
const database = dbValidation.database;
|
|
192
|
+
// Validate table name
|
|
193
|
+
if (!this.security.validateIdentifier(table_name).valid) {
|
|
194
|
+
return { status: 'error', error: 'Invalid table name' };
|
|
195
|
+
}
|
|
196
|
+
const query = `TRUNCATE TABLE \`${database}\`.\`${table_name}\``;
|
|
197
|
+
await this.db.query(query);
|
|
198
|
+
return {
|
|
199
|
+
status: 'success',
|
|
200
|
+
message: `Table '${table_name}' truncated successfully`,
|
|
201
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
status: 'error',
|
|
207
|
+
error: error.message,
|
|
208
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get table status and statistics
|
|
214
|
+
*/
|
|
215
|
+
async getTableStatus(params) {
|
|
216
|
+
try {
|
|
217
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
218
|
+
if (!dbValidation.valid) {
|
|
219
|
+
return { status: 'error', error: dbValidation.error };
|
|
220
|
+
}
|
|
221
|
+
const { table_name } = params;
|
|
222
|
+
const database = dbValidation.database;
|
|
223
|
+
let query = `SHOW TABLE STATUS FROM \`${database}\``;
|
|
224
|
+
if (table_name) {
|
|
225
|
+
if (!this.security.validateIdentifier(table_name).valid) {
|
|
226
|
+
return { status: 'error', error: 'Invalid table name' };
|
|
227
|
+
}
|
|
228
|
+
query += ` LIKE '${table_name}'`;
|
|
229
|
+
}
|
|
230
|
+
const results = await this.db.query(query);
|
|
231
|
+
// Format results for better readability
|
|
232
|
+
const formattedResults = results.map(row => ({
|
|
233
|
+
table_name: row.Name,
|
|
234
|
+
engine: row.Engine,
|
|
235
|
+
version: row.Version,
|
|
236
|
+
row_format: row.Row_format,
|
|
237
|
+
rows: row.Rows,
|
|
238
|
+
avg_row_length: row.Avg_row_length,
|
|
239
|
+
data_length: row.Data_length,
|
|
240
|
+
max_data_length: row.Max_data_length,
|
|
241
|
+
index_length: row.Index_length,
|
|
242
|
+
data_free: row.Data_free,
|
|
243
|
+
auto_increment: row.Auto_increment,
|
|
244
|
+
create_time: row.Create_time,
|
|
245
|
+
update_time: row.Update_time,
|
|
246
|
+
check_time: row.Check_time,
|
|
247
|
+
collation: row.Collation,
|
|
248
|
+
checksum: row.Checksum,
|
|
249
|
+
create_options: row.Create_options,
|
|
250
|
+
comment: row.Comment
|
|
251
|
+
}));
|
|
252
|
+
return {
|
|
253
|
+
status: 'success',
|
|
254
|
+
data: table_name ? formattedResults[0] : formattedResults,
|
|
255
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
return {
|
|
260
|
+
status: 'error',
|
|
261
|
+
error: error.message,
|
|
262
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Flush table (close and reopen)
|
|
268
|
+
*/
|
|
269
|
+
async flushTable(params) {
|
|
270
|
+
try {
|
|
271
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
272
|
+
if (!dbValidation.valid) {
|
|
273
|
+
return { status: 'error', error: dbValidation.error };
|
|
274
|
+
}
|
|
275
|
+
const { table_name, with_read_lock = false } = params;
|
|
276
|
+
const database = dbValidation.database;
|
|
277
|
+
let query;
|
|
278
|
+
if (table_name) {
|
|
279
|
+
if (!this.security.validateIdentifier(table_name).valid) {
|
|
280
|
+
return { status: 'error', error: 'Invalid table name' };
|
|
281
|
+
}
|
|
282
|
+
query = `FLUSH TABLES \`${database}\`.\`${table_name}\``;
|
|
283
|
+
if (with_read_lock) {
|
|
284
|
+
query += ' WITH READ LOCK';
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
query = with_read_lock ? 'FLUSH TABLES WITH READ LOCK' : 'FLUSH TABLES';
|
|
289
|
+
}
|
|
290
|
+
await this.db.query(query);
|
|
291
|
+
return {
|
|
292
|
+
status: 'success',
|
|
293
|
+
message: table_name
|
|
294
|
+
? `Table '${table_name}' flushed successfully${with_read_lock ? ' with read lock' : ''}`
|
|
295
|
+
: `All tables flushed successfully${with_read_lock ? ' with read lock' : ''}`,
|
|
296
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
status: 'error',
|
|
302
|
+
error: error.message,
|
|
303
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Get table size information
|
|
309
|
+
*/
|
|
310
|
+
async getTableSize(params) {
|
|
311
|
+
try {
|
|
312
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
313
|
+
if (!dbValidation.valid) {
|
|
314
|
+
return { status: 'error', error: dbValidation.error };
|
|
315
|
+
}
|
|
316
|
+
const { table_name } = params;
|
|
317
|
+
const database = dbValidation.database;
|
|
318
|
+
let query = `
|
|
319
|
+
SELECT
|
|
320
|
+
TABLE_NAME as table_name,
|
|
321
|
+
TABLE_ROWS as row_count,
|
|
322
|
+
DATA_LENGTH as data_size_bytes,
|
|
323
|
+
INDEX_LENGTH as index_size_bytes,
|
|
324
|
+
(DATA_LENGTH + INDEX_LENGTH) as total_size_bytes,
|
|
325
|
+
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) as total_size_mb,
|
|
326
|
+
DATA_FREE as free_space_bytes,
|
|
327
|
+
ENGINE as engine,
|
|
328
|
+
TABLE_COLLATION as collation
|
|
329
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
330
|
+
WHERE TABLE_SCHEMA = ?
|
|
331
|
+
`;
|
|
332
|
+
const queryParams = [database];
|
|
333
|
+
if (table_name) {
|
|
334
|
+
if (!this.security.validateIdentifier(table_name).valid) {
|
|
335
|
+
return { status: 'error', error: 'Invalid table name' };
|
|
336
|
+
}
|
|
337
|
+
query += ` AND TABLE_NAME = ?`;
|
|
338
|
+
queryParams.push(table_name);
|
|
339
|
+
}
|
|
340
|
+
query += ` ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC`;
|
|
341
|
+
const results = await this.db.query(query, queryParams);
|
|
342
|
+
// Calculate totals if getting all tables
|
|
343
|
+
let totalStats = null;
|
|
344
|
+
if (!table_name && results.length > 0) {
|
|
345
|
+
totalStats = {
|
|
346
|
+
total_tables: results.length,
|
|
347
|
+
total_rows: results.reduce((sum, r) => sum + (r.row_count || 0), 0),
|
|
348
|
+
total_data_size_bytes: results.reduce((sum, r) => sum + (r.data_size_bytes || 0), 0),
|
|
349
|
+
total_index_size_bytes: results.reduce((sum, r) => sum + (r.index_size_bytes || 0), 0),
|
|
350
|
+
total_size_mb: results.reduce((sum, r) => sum + (parseFloat(r.total_size_mb) || 0), 0).toFixed(2)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
status: 'success',
|
|
355
|
+
data: {
|
|
356
|
+
tables: table_name ? results[0] : results,
|
|
357
|
+
...(totalStats && { summary: totalStats })
|
|
358
|
+
},
|
|
359
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
status: 'error',
|
|
365
|
+
error: error.message,
|
|
366
|
+
queryLog: this.db.getFormattedQueryLogs(1)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
exports.MaintenanceTools = MaintenanceTools;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { SecurityLayer } from '../security/securityLayer';
|
|
2
|
+
export declare class ProcessTools {
|
|
3
|
+
private db;
|
|
4
|
+
private security;
|
|
5
|
+
constructor(security: SecurityLayer);
|
|
6
|
+
/**
|
|
7
|
+
* Show all running processes/connections
|
|
8
|
+
*/
|
|
9
|
+
showProcessList(params?: {
|
|
10
|
+
full?: boolean;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
status: string;
|
|
13
|
+
data?: any[];
|
|
14
|
+
error?: string;
|
|
15
|
+
queryLog?: string;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Kill a specific process/connection
|
|
19
|
+
*/
|
|
20
|
+
killProcess(params: {
|
|
21
|
+
process_id: number;
|
|
22
|
+
type?: 'CONNECTION' | 'QUERY';
|
|
23
|
+
}): Promise<{
|
|
24
|
+
status: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
queryLog?: string;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Show server status variables
|
|
31
|
+
*/
|
|
32
|
+
showStatus(params?: {
|
|
33
|
+
like?: string;
|
|
34
|
+
global?: boolean;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
status: string;
|
|
37
|
+
data?: any;
|
|
38
|
+
error?: string;
|
|
39
|
+
queryLog?: string;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Show server variables
|
|
43
|
+
*/
|
|
44
|
+
showVariables(params?: {
|
|
45
|
+
like?: string;
|
|
46
|
+
global?: boolean;
|
|
47
|
+
}): Promise<{
|
|
48
|
+
status: string;
|
|
49
|
+
data?: any;
|
|
50
|
+
error?: string;
|
|
51
|
+
queryLog?: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Explain a query (show execution plan)
|
|
55
|
+
*/
|
|
56
|
+
explainQuery(params: {
|
|
57
|
+
query: string;
|
|
58
|
+
format?: 'TRADITIONAL' | 'JSON' | 'TREE';
|
|
59
|
+
analyze?: boolean;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
status: string;
|
|
62
|
+
data?: any;
|
|
63
|
+
error?: string;
|
|
64
|
+
queryLog?: string;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Show engine status (InnoDB, etc.)
|
|
68
|
+
*/
|
|
69
|
+
showEngineStatus(params?: {
|
|
70
|
+
engine?: string;
|
|
71
|
+
}): Promise<{
|
|
72
|
+
status: string;
|
|
73
|
+
data?: any;
|
|
74
|
+
error?: string;
|
|
75
|
+
queryLog?: string;
|
|
76
|
+
}>;
|
|
77
|
+
/**
|
|
78
|
+
* Get server information
|
|
79
|
+
*/
|
|
80
|
+
getServerInfo(): Promise<{
|
|
81
|
+
status: string;
|
|
82
|
+
data?: any;
|
|
83
|
+
error?: string;
|
|
84
|
+
queryLog?: string;
|
|
85
|
+
}>;
|
|
86
|
+
/**
|
|
87
|
+
* Show binary logs
|
|
88
|
+
*/
|
|
89
|
+
showBinaryLogs(): Promise<{
|
|
90
|
+
status: string;
|
|
91
|
+
data?: any[];
|
|
92
|
+
error?: string;
|
|
93
|
+
queryLog?: string;
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* Show master/replica status
|
|
97
|
+
*/
|
|
98
|
+
showReplicationStatus(params?: {
|
|
99
|
+
type?: 'MASTER' | 'REPLICA' | 'SLAVE';
|
|
100
|
+
}): Promise<{
|
|
101
|
+
status: string;
|
|
102
|
+
data?: any;
|
|
103
|
+
error?: string;
|
|
104
|
+
queryLog?: string;
|
|
105
|
+
}>;
|
|
106
|
+
}
|