editdb-cli 1.0.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/src/ui.js ADDED
@@ -0,0 +1,418 @@
1
+ import inquirer from 'inquirer';
2
+ import {
3
+ getTables,
4
+ getPrimaryKey,
5
+ getSingleRow,
6
+ getTableInfo,
7
+ countRows,
8
+ getRowsPaginated,
9
+ } from './db.js';
10
+ import {
11
+ updateRow,
12
+ deleteRow,
13
+ insertRow,
14
+ addColumn,
15
+ dropColumn,
16
+ renameTable,
17
+ searchRows,
18
+ updateMultipleRows,
19
+ deleteMultipleRows,
20
+ } from './actions.js';
21
+ import { logger } from './utils.js';
22
+ import { config } from './config.js';
23
+ import { t } from './i18n.js';
24
+ import {
25
+ CHOICES,
26
+ PAGINATION,
27
+ UI_ACTIONS,
28
+ ROW_ACTIONS,
29
+ ALTER_TABLE_ACTIONS,
30
+ MULTI_ROW_ACTIONS,
31
+ } from './constants.js';
32
+
33
+ const PAGE_SIZE = config.PAGE_SIZE;
34
+
35
+ export async function selectTable(db) {
36
+ const tables = getTables(db);
37
+
38
+ const choices = [];
39
+ const tableCountMessage =
40
+ tables.length > 0
41
+ ? t('ui.selectTable', { count: tables.length })
42
+ : t('ui.selectTableNotFound');
43
+
44
+ if (tables.length > 0) {
45
+ choices.push(new inquirer.Separator('--- Existing Tables ---'));
46
+ choices.push(...tables);
47
+ }
48
+
49
+ choices.push(
50
+ new inquirer.Separator(),
51
+ { name: t('ui.newTableChoice'), value: CHOICES.TABLE_NEW },
52
+ { name: t('actions.customQueryHeader'), value: CHOICES.TABLE_CUSTOM_QUERY },
53
+ { name: t('ui.exitChoice'), value: CHOICES.EXIT }
54
+ );
55
+
56
+ const { table } = await inquirer.prompt([
57
+ {
58
+ type: 'list',
59
+ name: 'table',
60
+ message: tableCountMessage,
61
+ choices: choices,
62
+ },
63
+ ]);
64
+
65
+ return table;
66
+ }
67
+
68
+ async function selectRow(db, table) {
69
+ const totalRows = countRows(db, table);
70
+ if (totalRows === 0) {
71
+ logger.warn(t('ui.noRows'));
72
+ return {};
73
+ }
74
+
75
+ let idKey = getPrimaryKey(db, table);
76
+ if (!idKey) {
77
+ logger.warn(t('ui.pkNotFound'));
78
+ const columns = getTableInfo(db, table).map((c) => c.name);
79
+ ({ idKey } = await inquirer.prompt([
80
+ {
81
+ type: 'list',
82
+ name: 'idKey',
83
+ message: t('ui.selectUniqueColumn'),
84
+ choices: columns,
85
+ },
86
+ ]));
87
+ } else {
88
+ logger.info(t('ui.pkFound', { idKey }));
89
+ }
90
+
91
+ const PAGE_SIZE = config.PAGE_SIZE;
92
+ let offset = 0;
93
+
94
+ while (true) {
95
+ const rows = getRowsPaginated(db, table, PAGE_SIZE, offset);
96
+ const currentPage = Math.floor(offset / PAGE_SIZE) + 1;
97
+ const totalPages = Math.ceil(totalRows / PAGE_SIZE);
98
+
99
+ const { rowId } = await inquirer.prompt([
100
+ {
101
+ type: 'list',
102
+ name: 'rowId',
103
+ message: t('ui.selectRow', { currentPage, totalPages }),
104
+ choices: [
105
+ ...rows.map((r) => {
106
+ const displayKeys = Object.keys(r)
107
+ .filter((k) => k !== idKey)
108
+ .slice(0, 3);
109
+ const displayPairs = displayKeys
110
+ .map((k) => {
111
+ let val = r[k];
112
+ if (typeof val === 'string' && val.length > 20) {
113
+ val = val.substring(0, 17) + '...';
114
+ } else if (val === null) {
115
+ val = 'NULL';
116
+ }
117
+ return `${k}: ${val}`;
118
+ })
119
+ .join(' | ');
120
+
121
+ const remainingCount =
122
+ Object.keys(r).length -
123
+ displayKeys.length -
124
+ (Object.keys(r).includes(idKey) ? 1 : 0);
125
+ const moreText =
126
+ remainingCount > 0 ? ` (+${remainingCount} more)` : '';
127
+
128
+ return {
129
+ name: `${idKey}: ${r[idKey]} | ${displayPairs}${moreText}`,
130
+ value: r[idKey],
131
+ };
132
+ }),
133
+ new inquirer.Separator(),
134
+ {
135
+ name: t('ui.nextPage'),
136
+ value: PAGINATION.NEXT,
137
+ disabled: offset + PAGE_SIZE >= totalRows,
138
+ },
139
+ { name: t('ui.prevPage'), value: PAGINATION.PREV, disabled: offset === 0 },
140
+ { name: t('common.back'), value: UI_ACTIONS.BACK },
141
+ ],
142
+ },
143
+ ]);
144
+
145
+ if (rowId === PAGINATION.NEXT) {
146
+ offset += PAGE_SIZE;
147
+ } else if (rowId === PAGINATION.PREV) {
148
+ offset -= PAGE_SIZE;
149
+ } else if (rowId === UI_ACTIONS.BACK) {
150
+ return {};
151
+ } else {
152
+ return { idKey, row: getSingleRow(db, table, idKey, rowId) };
153
+ }
154
+ }
155
+ }
156
+
157
+ async function rowActionPrompt(db, table, idKey, row) {
158
+ let currentRow = row;
159
+ while (true) {
160
+ logger.log(t('ui.selectedRow'), currentRow);
161
+ const { action } = await inquirer.prompt([
162
+ {
163
+ type: 'list',
164
+ name: 'action',
165
+ message: t('ui.rowAction'),
166
+ choices: [
167
+ { name: t('ui.editColumn'), value: ROW_ACTIONS.EDIT },
168
+ { name: t('ui.deleteRow'), value: ROW_ACTIONS.DELETE },
169
+ new inquirer.Separator(),
170
+ { name: t('ui.selectAnotherRow'), value: UI_ACTIONS.BACK },
171
+ ],
172
+ },
173
+ ]);
174
+
175
+ if (action === ROW_ACTIONS.EDIT) {
176
+ await updateRow(db, table, idKey, currentRow);
177
+ const updatedRow = getSingleRow(db, table, idKey, currentRow[idKey]);
178
+ if (updatedRow) {
179
+ currentRow = updatedRow;
180
+ } else {
181
+ logger.warn(t('actions.deleteWarning'));
182
+ return;
183
+ }
184
+ } else if (action === ROW_ACTIONS.DELETE) {
185
+ await deleteRow(db, table, idKey, currentRow[idKey]);
186
+ return;
187
+ } else if (action === UI_ACTIONS.BACK) {
188
+ return;
189
+ }
190
+ }
191
+ }
192
+
193
+ function displaySchema(db, table) {
194
+ const schema = getTableInfo(db, table);
195
+ logger.info(t('ui.schemaHeader', { table }));
196
+ console.table(schema);
197
+ logger.info(t('ui.schemaFooter'));
198
+ }
199
+
200
+ async function alterTablePrompt(db, table) {
201
+ let currentTable = table;
202
+ while (true) {
203
+ displaySchema(db, currentTable);
204
+ const { action } = await inquirer.prompt([
205
+ {
206
+ type: 'list',
207
+ name: 'action',
208
+ message: t('ui.alterTable.message'),
209
+ choices: [
210
+ {
211
+ name: t('ui.alterTable.addColumn'),
212
+ value: ALTER_TABLE_ACTIONS.ADD_COLUMN,
213
+ },
214
+ {
215
+ name: t('ui.alterTable.dropColumn'),
216
+ value: ALTER_TABLE_ACTIONS.DROP_COLUMN,
217
+ },
218
+ {
219
+ name: t('ui.alterTable.renameTable'),
220
+ value: ALTER_TABLE_ACTIONS.RENAME_TABLE,
221
+ },
222
+ new inquirer.Separator(),
223
+ { name: t('common.back'), value: UI_ACTIONS.BACK },
224
+ ],
225
+ },
226
+ ]);
227
+
228
+ switch (action) {
229
+ case ALTER_TABLE_ACTIONS.ADD_COLUMN:
230
+ await addColumn(db, currentTable);
231
+ break;
232
+ case ALTER_TABLE_ACTIONS.DROP_COLUMN:
233
+ await dropColumn(db, currentTable);
234
+ break;
235
+ case ALTER_TABLE_ACTIONS.RENAME_TABLE:
236
+ const newTableName = await renameTable(db, currentTable);
237
+ if (newTableName) {
238
+ currentTable = newTableName;
239
+ }
240
+ break;
241
+ case UI_ACTIONS.BACK:
242
+ return currentTable;
243
+ }
244
+ }
245
+ }
246
+
247
+ async function buildWhereClause(db, table) {
248
+ const columns = getTableInfo(db, table).map((c) => c.name);
249
+ const conditions = [];
250
+ const params = [];
251
+
252
+ let addMore = true;
253
+ while (addMore) {
254
+ const { column } = await inquirer.prompt([
255
+ {
256
+ type: 'list',
257
+ name: 'column',
258
+ message: t('ui.multiRow.selectColumn'),
259
+ choices: columns,
260
+ },
261
+ ]);
262
+
263
+ const { operator } = await inquirer.prompt([
264
+ {
265
+ type: 'list',
266
+ name: 'operator',
267
+ message: t('ui.multiRow.selectOperator'),
268
+ choices: [
269
+ '=',
270
+ '!=',
271
+ '>',
272
+ '<',
273
+ '>=',
274
+ '<=',
275
+ 'LIKE',
276
+ 'IN',
277
+ 'IS NULL',
278
+ 'IS NOT NULL',
279
+ ],
280
+ },
281
+ ]);
282
+
283
+ let value;
284
+ if (operator !== 'IS NULL' && operator !== 'IS NOT NULL') {
285
+ ({ value } = await inquirer.prompt([
286
+ {
287
+ type: 'input',
288
+ name: 'value',
289
+ message: t('ui.multiRow.enterValue'),
290
+ },
291
+ ]));
292
+ }
293
+
294
+ let condition;
295
+ if (operator === 'IN') {
296
+ const values = value.split(',').map((v) => v.trim());
297
+ condition = `"${column}" IN (${values.map(() => '?').join(',')})`;
298
+ params.push(...values);
299
+ } else if (operator === 'IS NULL' || operator === 'IS NOT NULL') {
300
+ condition = `"${column}" ${operator}`;
301
+ } else {
302
+ condition = `"${column}" ${operator} ?`;
303
+ params.push(value);
304
+ }
305
+ conditions.push(condition);
306
+
307
+ ({ addMore } = await inquirer.prompt([
308
+ {
309
+ type: 'confirm',
310
+ name: 'addMore',
311
+ message: t('ui.multiRow.addMoreConditions'),
312
+ default: false,
313
+ },
314
+ ]));
315
+ }
316
+
317
+ return { whereClause: conditions.join(' AND '), params };
318
+ }
319
+
320
+ async function multiRowActionPrompt(db, table) {
321
+ const { whereClause, params } = await buildWhereClause(db, table);
322
+
323
+ if (!whereClause) {
324
+ logger.warn(t('ui.multiRow.whereEmpty'));
325
+ return;
326
+ }
327
+
328
+ try {
329
+ const rows = searchRows(db, table, whereClause, params);
330
+ if (rows.length === 0) {
331
+ logger.warn(t('ui.multiRow.noRowsFound'));
332
+ return;
333
+ }
334
+
335
+ logger.info(t('ui.multiRow.rowsFound', { count: rows.length }));
336
+ console.table(rows);
337
+
338
+ const { action } = await inquirer.prompt([
339
+ {
340
+ type: 'list',
341
+ name: 'action',
342
+ message: t('ui.multiRow.actionPrompt'),
343
+ choices: [
344
+ {
345
+ name: t('ui.multiRow.updateAll'),
346
+ value: MULTI_ROW_ACTIONS.UPDATE,
347
+ },
348
+ {
349
+ name: t('ui.multiRow.deleteAll'),
350
+ value: MULTI_ROW_ACTIONS.DELETE,
351
+ },
352
+ new inquirer.Separator(),
353
+ { name: t('common.back'), value: UI_ACTIONS.BACK },
354
+ ],
355
+ },
356
+ ]);
357
+
358
+ if (action === MULTI_ROW_ACTIONS.UPDATE) {
359
+ await updateMultipleRows(db, table, whereClause, params, rows.length);
360
+ } else if (action === MULTI_ROW_ACTIONS.DELETE) {
361
+ await deleteMultipleRows(db, table, whereClause, params, rows.length);
362
+ }
363
+ } catch (err) {
364
+ logger.error(t('actions.customQueryError', { message: err.message }));
365
+ }
366
+ }
367
+
368
+ export async function tableActionPrompt(db, table) {
369
+ let currentTable = table;
370
+ displaySchema(db, currentTable);
371
+
372
+ while (true) {
373
+ const { action } = await inquirer.prompt([
374
+ {
375
+ type: 'list',
376
+ name: 'action',
377
+ message: t('ui.tableAction', { table: currentTable }),
378
+ choices: [
379
+ { name: t('ui.selectRowAction'), value: UI_ACTIONS.ROW_SELECT },
380
+ { name: t('ui.multiRowAction'), value: UI_ACTIONS.ROW_MULTI_ACTION },
381
+ { name: t('ui.insertRowAction'), value: UI_ACTIONS.ROW_INSERT },
382
+ { name: t('ui.alterTableAction'), value: UI_ACTIONS.TABLE_ALTER },
383
+ { name: t('ui.showSchemaAction'), value: UI_ACTIONS.TABLE_SCHEMA },
384
+ new inquirer.Separator(),
385
+ { name: t('ui.selectAnotherTable'), value: UI_ACTIONS.BACK },
386
+ ],
387
+ },
388
+ ]);
389
+
390
+ if (action === UI_ACTIONS.ROW_SELECT) {
391
+ const { idKey, row } = await selectRow(db, currentTable);
392
+ if (row && idKey) {
393
+ await rowActionPrompt(db, currentTable, idKey, row);
394
+ }
395
+ } else if (action === UI_ACTIONS.ROW_MULTI_ACTION) {
396
+ await multiRowActionPrompt(db, currentTable);
397
+ } else if (action === UI_ACTIONS.ROW_INSERT) {
398
+ await insertRow(db, currentTable);
399
+ } else if (action === UI_ACTIONS.TABLE_ALTER) {
400
+ currentTable = await alterTablePrompt(db, currentTable);
401
+ } else if (action === UI_ACTIONS.TABLE_SCHEMA) {
402
+ displaySchema(db, currentTable);
403
+ } else if (action === UI_ACTIONS.BACK) {
404
+ return;
405
+ }
406
+ } else if (action === 'multi') {
407
+ await multiRowActionPrompt(db, currentTable);
408
+ } else if (action === 'insert') {
409
+ await insertRow(db, currentTable);
410
+ } else if (action === 'alter') {
411
+ currentTable = await alterTablePrompt(db, currentTable);
412
+ } else if (action === 'schema') {
413
+ displaySchema(db, currentTable);
414
+ } else if (action === 'back') {
415
+ return;
416
+ }
417
+ }
418
+ }
package/src/utils.js ADDED
@@ -0,0 +1,63 @@
1
+ import chalk from 'chalk';
2
+
3
+ export function parseValueBasedOnOriginal(inputValue, originalValue) {
4
+ if (inputValue.toLowerCase() === 'null') return null;
5
+ if (inputValue === '' && originalValue !== null) return '';
6
+
7
+ const originalType = typeof originalValue;
8
+
9
+ if (originalType === 'number') {
10
+ const num = Number(inputValue);
11
+ return isNaN(num) ? undefined : num;
12
+ }
13
+
14
+ if (originalType === 'bigint') {
15
+ try {
16
+ return BigInt(inputValue);
17
+ } catch (e) {
18
+ return undefined;
19
+ }
20
+ }
21
+
22
+ return inputValue;
23
+ }
24
+
25
+ export function parseValueBasedOnSchema(inputValue, schemaType) {
26
+ const upperType = schemaType.toUpperCase();
27
+ if (inputValue.toLowerCase() === 'null') return null;
28
+ if (inputValue === '') return null;
29
+
30
+ if (upperType.includes('INT')) {
31
+ try {
32
+ const num = Number(inputValue);
33
+ return isNaN(num) ? undefined : num;
34
+ } catch (e) {
35
+ return undefined;
36
+ }
37
+ }
38
+ if (
39
+ upperType.includes('REAL') ||
40
+ upperType.includes('FLOAT') ||
41
+ upperType.includes('DOUB')
42
+ ) {
43
+ const num = Number(inputValue);
44
+ return isNaN(num) ? undefined : num;
45
+ }
46
+
47
+ return inputValue;
48
+ }
49
+
50
+ export function isValidIdentifier(name) {
51
+ if (!name || typeof name !== 'string') {
52
+ return false;
53
+ }
54
+ return /^[A-Za-z0-9_\s.-]*[A-Za-z0-9_-]+[A-Za-z0-9_\s.-]*$/.test(name);
55
+ }
56
+
57
+ export const logger = {
58
+ info: (message) => console.log(chalk.blue(message)),
59
+ success: (message) => console.log(chalk.green(message)),
60
+ warn: (message) => console.log(chalk.yellow(message)),
61
+ error: (message) => console.error(chalk.red(message)),
62
+ log: (message) => console.log(message),
63
+ };