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/.eslintrc.js +12 -0
- package/.prettierrc.json +6 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/README_ja.md +50 -0
- package/assets/help_message_en.txt +11 -0
- package/assets/help_message_ja.txt +11 -0
- package/bin/editdb.js +7 -0
- package/counts.db +0 -0
- package/docs/CONTRIBUTING.md +38 -0
- package/docs/CONTRIBUTING_ja.md +38 -0
- package/docs/FEATURES.md +35 -0
- package/docs/FEATURES_ja.md +35 -0
- package/locales/en.json +147 -0
- package/locales/ja.json +147 -0
- package/package.json +34 -0
- package/src/__tests__/actions.test.js +399 -0
- package/src/__tests__/db.test.js +128 -0
- package/src/__tests__/utils.test.js +80 -0
- package/src/actions.js +691 -0
- package/src/config.js +3 -0
- package/src/constants.js +36 -0
- package/src/db.js +176 -0
- package/src/i18n.js +62 -0
- package/src/index.js +95 -0
- package/src/ui.js +418 -0
- package/src/utils.js +63 -0
package/src/constants.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const CHOICES = {
|
|
2
|
+
DB_NEW: '__NEW__',
|
|
3
|
+
TABLE_NEW: '__NEW_TABLE__',
|
|
4
|
+
TABLE_CUSTOM_QUERY: '__CUSTOM_QUERY__',
|
|
5
|
+
EXIT: null,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const UI_ACTIONS = {
|
|
9
|
+
ROW_SELECT: 'select',
|
|
10
|
+
ROW_MULTI_ACTION: 'multi',
|
|
11
|
+
ROW_INSERT: 'insert',
|
|
12
|
+
TABLE_ALTER: 'alter',
|
|
13
|
+
TABLE_SCHEMA: 'schema',
|
|
14
|
+
BACK: 'back',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const ROW_ACTIONS = {
|
|
18
|
+
EDIT: 'edit',
|
|
19
|
+
DELETE: 'delete',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const PAGINATION = {
|
|
23
|
+
NEXT: '__NEXT__',
|
|
24
|
+
PREV: '__PREV__',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ALTER_TABLE_ACTIONS = {
|
|
28
|
+
ADD_COLUMN: 'add_column',
|
|
29
|
+
DROP_COLUMN: 'drop_column',
|
|
30
|
+
RENAME_TABLE: 'rename_table',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const MULTI_ROW_ACTIONS = {
|
|
34
|
+
UPDATE: 'update',
|
|
35
|
+
DELETE: 'delete',
|
|
36
|
+
};
|
package/src/db.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import { logger, isValidIdentifier } from './utils.js';
|
|
6
|
+
import { CHOICES } from './constants.js';
|
|
7
|
+
import { t } from './i18n.js';
|
|
8
|
+
|
|
9
|
+
async function fileExists(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
await fs.access(filePath);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function selectDatabase(dbPath) {
|
|
19
|
+
if (dbPath) {
|
|
20
|
+
const resolvedPath = path.resolve(process.cwd(), dbPath);
|
|
21
|
+
if (!(await fileExists(resolvedPath))) {
|
|
22
|
+
const { createNew } = await inquirer.prompt([
|
|
23
|
+
{
|
|
24
|
+
type: 'confirm',
|
|
25
|
+
name: 'createNew',
|
|
26
|
+
message: t('db.notFound', { dbPath }),
|
|
27
|
+
default: true,
|
|
28
|
+
},
|
|
29
|
+
]);
|
|
30
|
+
if (!createNew) {
|
|
31
|
+
logger.warn(t('common.cancel'));
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!resolvedPath.endsWith('.db') && !resolvedPath.endsWith('.sqlite')) {
|
|
35
|
+
logger.error(t('db.fileTypeError'));
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
logger.info(t('db.newDb', { dbName: path.basename(resolvedPath) }));
|
|
39
|
+
}
|
|
40
|
+
const db = new Database(resolvedPath);
|
|
41
|
+
logger.success(t('db.connected', { dbName: path.basename(resolvedPath) }));
|
|
42
|
+
return db;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dbDir = process.cwd();
|
|
46
|
+
const files = await fs.readdir(dbDir);
|
|
47
|
+
const dbFiles = files.filter(
|
|
48
|
+
(f) => f.endsWith('.db') || f.endsWith('.sqlite')
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const databaseCountMessage =
|
|
52
|
+
dbFiles.length > 0
|
|
53
|
+
? t('db.selectDb', { count: dbFiles.length })
|
|
54
|
+
: t('db.selectDbNotFound');
|
|
55
|
+
|
|
56
|
+
const { dbFile } = await inquirer.prompt([
|
|
57
|
+
{
|
|
58
|
+
type: 'list',
|
|
59
|
+
name: 'dbFile',
|
|
60
|
+
message: databaseCountMessage,
|
|
61
|
+
choices: [
|
|
62
|
+
...dbFiles,
|
|
63
|
+
new inquirer.Separator(),
|
|
64
|
+
{ name: t('db.newDbChoice'), value: CHOICES.DB_NEW },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
let resolvedDbPath;
|
|
70
|
+
if (dbFile === CHOICES.DB_NEW) {
|
|
71
|
+
const { newDbName } = await inquirer.prompt([
|
|
72
|
+
{
|
|
73
|
+
type: 'input',
|
|
74
|
+
name: 'newDbName',
|
|
75
|
+
message: t('db.newDbPrompt'),
|
|
76
|
+
validate: async (input) => {
|
|
77
|
+
if (!input) return t('db.newDbValidateEmpty');
|
|
78
|
+
if (!input.endsWith('.db') && !input.endsWith('.sqlite')) {
|
|
79
|
+
return t('db.fileTypeError');
|
|
80
|
+
}
|
|
81
|
+
if (await fileExists(path.resolve(dbDir, input))) {
|
|
82
|
+
return t('db.newDbValidateExists');
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
resolvedDbPath = path.resolve(dbDir, newDbName);
|
|
89
|
+
logger.info(t('db.newDb', { dbName: newDbName }));
|
|
90
|
+
} else {
|
|
91
|
+
resolvedDbPath = path.resolve(dbDir, dbFile);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const db = new Database(resolvedDbPath);
|
|
95
|
+
logger.success(t('db.connected', { dbName: path.basename(resolvedDbPath) }));
|
|
96
|
+
return db;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function closeDatabase(db) {
|
|
100
|
+
db.close();
|
|
101
|
+
logger.info(`\n${t('db.connectionClosed')}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getTables(db) {
|
|
105
|
+
return db
|
|
106
|
+
.prepare(
|
|
107
|
+
`
|
|
108
|
+
SELECT name FROM sqlite_master
|
|
109
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
110
|
+
`
|
|
111
|
+
)
|
|
112
|
+
.all()
|
|
113
|
+
.map((t) => t.name);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getPrimaryKey(db, table) {
|
|
117
|
+
if (!isValidIdentifier(table))
|
|
118
|
+
throw new Error(`Invalid table name: ${table}`);
|
|
119
|
+
try {
|
|
120
|
+
const info = db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
121
|
+
const pk = info.find((col) => col.pk === 1);
|
|
122
|
+
return pk ? pk.name : null;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
logger.error(t('db.tableInfoError', { message: err.message }));
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getAllRows(db, table) {
|
|
130
|
+
if (!isValidIdentifier(table))
|
|
131
|
+
throw new Error(`Invalid table name: ${table}`);
|
|
132
|
+
return db.prepare(`SELECT * FROM "${table}"`).all();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function countRows(db, table) {
|
|
136
|
+
if (!isValidIdentifier(table))
|
|
137
|
+
throw new Error(`Invalid table name: ${table}`);
|
|
138
|
+
const { count } = db
|
|
139
|
+
.prepare(`SELECT COUNT(*) as count FROM "${table}"`)
|
|
140
|
+
.get();
|
|
141
|
+
return count;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getRowsPaginated(db, table, limit, offset) {
|
|
145
|
+
if (!isValidIdentifier(table))
|
|
146
|
+
throw new Error(`Invalid table name: ${table}`);
|
|
147
|
+
return db
|
|
148
|
+
.prepare(`SELECT * FROM "${table}" LIMIT ? OFFSET ?`)
|
|
149
|
+
.all(limit, offset);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getSingleRow(db, table, idKey, rowId) {
|
|
153
|
+
if (!isValidIdentifier(table))
|
|
154
|
+
throw new Error(`Invalid table name: ${table}`);
|
|
155
|
+
if (!isValidIdentifier(idKey))
|
|
156
|
+
throw new Error(`Invalid column name: ${idKey}`);
|
|
157
|
+
return db.prepare(`SELECT * FROM "${table}" WHERE "${idKey}" = ?`).get(rowId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getTableInfo(db, table) {
|
|
161
|
+
if (!isValidIdentifier(table))
|
|
162
|
+
throw new Error(`Invalid table name: ${table}`);
|
|
163
|
+
return db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function executeCustomQuery(db, sql, params = []) {
|
|
167
|
+
const trimmedSql = sql.trim().toUpperCase();
|
|
168
|
+
if (!trimmedSql.startsWith('SELECT')) {
|
|
169
|
+
throw new Error(t('db.readOnlyError'));
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return db.prepare(sql).all(params);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
throw new Error(t('db.queryError', { message: err.message }));
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/i18n.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
let translations = {};
|
|
5
|
+
let currentLang = 'en';
|
|
6
|
+
|
|
7
|
+
function resolve(path, obj) {
|
|
8
|
+
return path.split('.').reduce((prev, curr) => {
|
|
9
|
+
return prev ? prev[curr] : null;
|
|
10
|
+
}, obj || self);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function init(language) {
|
|
14
|
+
let lang = language;
|
|
15
|
+
|
|
16
|
+
if (!lang) {
|
|
17
|
+
const envLang = process.env.LANG || 'en_US.UTF-8';
|
|
18
|
+
lang = envLang.split('.')[0].split('_')[0];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!['en', 'ja'].includes(lang)) {
|
|
22
|
+
lang = 'en';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
currentLang = lang;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const filePath = path.resolve(process.cwd(), `locales/${currentLang}.json`);
|
|
29
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
30
|
+
translations = JSON.parse(fileContent);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(
|
|
33
|
+
`Could not load translations for '${currentLang}'. Falling back to English.`
|
|
34
|
+
);
|
|
35
|
+
currentLang = 'en';
|
|
36
|
+
const filePath = path.resolve(process.cwd(), `locales/en.json`);
|
|
37
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
38
|
+
translations = JSON.parse(fileContent);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function t(key, replacements = {}) {
|
|
43
|
+
let translation = resolve(key, translations);
|
|
44
|
+
|
|
45
|
+
if (!translation) {
|
|
46
|
+
console.warn(`Translation not found for key: ${key}`);
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const placeholder in replacements) {
|
|
51
|
+
translation = translation.replace(
|
|
52
|
+
new RegExp(`{${placeholder}}`, 'g'),
|
|
53
|
+
replacements[placeholder]
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return translation;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getLang() {
|
|
61
|
+
return currentLang;
|
|
62
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { selectDatabase, closeDatabase } from './db.js';
|
|
2
|
+
import { selectTable, tableActionPrompt } from './ui.js';
|
|
3
|
+
import { createTable, executeCustomQueryAction } from './actions.js';
|
|
4
|
+
import { logger } from './utils.js';
|
|
5
|
+
import { init, getLang, t } from './i18n.js';
|
|
6
|
+
import { CHOICES } from './constants.js';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import yargs from 'yargs';
|
|
11
|
+
import { hideBin } from 'yargs/helpers';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const meta = await getMetaData();
|
|
17
|
+
|
|
18
|
+
async function displayHelp() {
|
|
19
|
+
try {
|
|
20
|
+
const lang = getLang();
|
|
21
|
+
const helpMessagePath = path.resolve(
|
|
22
|
+
__dirname,
|
|
23
|
+
`../assets/help_message_${lang}.txt`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const helpContent = await fs.readFile(helpMessagePath, 'utf-8');
|
|
27
|
+
|
|
28
|
+
const rendered = helpContent
|
|
29
|
+
.replace(/\$\{meta\.version\}/g, meta.version ?? '')
|
|
30
|
+
.replace(/\$\{meta\.repositoryURL\}/g, meta.repository.url ?? '');
|
|
31
|
+
|
|
32
|
+
console.log(rendered);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.error(t('common.error'), error.message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getMetaData() {
|
|
39
|
+
const packageJsonPath = path.resolve(__dirname, '../package.json');
|
|
40
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
|
|
41
|
+
return JSON.parse(packageJsonContent);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function main() {
|
|
45
|
+
const args = hideBin(process.argv);
|
|
46
|
+
|
|
47
|
+
const langArg = yargs(args.filter((a) => a !== '-h' && a !== '--help'))
|
|
48
|
+
.option('lang', { alias: 'l', type: 'string', choices: ['en', 'ja'] })
|
|
49
|
+
.help(false)
|
|
50
|
+
.version(false)
|
|
51
|
+
.parseSync();
|
|
52
|
+
await init(langArg.lang || null);
|
|
53
|
+
|
|
54
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
55
|
+
await displayHelp();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const argv = yargs(args)
|
|
60
|
+
.version(meta.version)
|
|
61
|
+
.alias('v', 'version')
|
|
62
|
+
.positional('dbPath', {
|
|
63
|
+
describe: t('common.dbpath_description'),
|
|
64
|
+
type: 'string',
|
|
65
|
+
})
|
|
66
|
+
.help(false)
|
|
67
|
+
.parse();
|
|
68
|
+
|
|
69
|
+
let db;
|
|
70
|
+
try {
|
|
71
|
+
db = await selectDatabase(argv.dbPath);
|
|
72
|
+
if (!db) return;
|
|
73
|
+
|
|
74
|
+
while (true) {
|
|
75
|
+
const table = await selectTable(db);
|
|
76
|
+
|
|
77
|
+
if (table === CHOICES.TABLE_NEW) {
|
|
78
|
+
await createTable(db);
|
|
79
|
+
} else if (table === CHOICES.TABLE_CUSTOM_QUERY) {
|
|
80
|
+
await executeCustomQueryAction(db);
|
|
81
|
+
} else if (table) {
|
|
82
|
+
await tableActionPrompt(db, table);
|
|
83
|
+
} else {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.error(`\n${t('common.error')}:`, err.message);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
} finally {
|
|
91
|
+
if (db) {
|
|
92
|
+
closeDatabase(db);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|