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/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "editdb-cli",
|
|
3
|
+
"version": "v1.0.0",
|
|
4
|
+
"description": "A CLI tool that allows anyone to easily edit and create SQLite databases. SQLiteデータベースを誰でも簡単に編集、作成できるCLIツール。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "yh2237",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/yh2237/EditDB-CLI"
|
|
11
|
+
},
|
|
12
|
+
"main": "src/index.js",
|
|
13
|
+
"bin": {
|
|
14
|
+
"editdb": "bin/editdb.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "vitest",
|
|
18
|
+
"lint": "eslint .",
|
|
19
|
+
"format": "prettier --write ."
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"better-sqlite3": "^12.5.0",
|
|
23
|
+
"chalk": "^5.6.2",
|
|
24
|
+
"inquirer": "^8.2.4",
|
|
25
|
+
"yargs": "^18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
29
|
+
"eslint": "^9.39.2",
|
|
30
|
+
"eslint-config-prettier": "^10.1.8",
|
|
31
|
+
"prettier": "^3.8.1",
|
|
32
|
+
"vitest": "^4.0.16"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import {
|
|
4
|
+
createTable,
|
|
5
|
+
insertRow,
|
|
6
|
+
updateRow,
|
|
7
|
+
deleteRow,
|
|
8
|
+
addColumn,
|
|
9
|
+
dropColumn,
|
|
10
|
+
renameTable,
|
|
11
|
+
searchRows,
|
|
12
|
+
updateMultipleRows,
|
|
13
|
+
deleteMultipleRows,
|
|
14
|
+
} from '../actions.js';
|
|
15
|
+
import * as dbModule from '../db.js';
|
|
16
|
+
import * as utilsModule from '../utils.js';
|
|
17
|
+
import * as inquirerModule from 'inquirer';
|
|
18
|
+
import * as i18nModule from '../i18n.js';
|
|
19
|
+
|
|
20
|
+
vi.mock('../i18n.js', () => ({
|
|
21
|
+
t: vi.fn((key) => key),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('../utils.js', () => ({
|
|
25
|
+
logger: {
|
|
26
|
+
info: vi.fn(),
|
|
27
|
+
success: vi.fn(),
|
|
28
|
+
warn: vi.fn(),
|
|
29
|
+
error: vi.fn(),
|
|
30
|
+
log: vi.fn(),
|
|
31
|
+
},
|
|
32
|
+
isValidIdentifier: vi.fn((input) => /^[a-zA-Z0-9_]+$/.test(input)),
|
|
33
|
+
parseValueBasedOnOriginal: vi.fn((input, original) => {
|
|
34
|
+
if (typeof original === 'number' && !isNaN(Number(input)))
|
|
35
|
+
return Number(input);
|
|
36
|
+
return input;
|
|
37
|
+
}),
|
|
38
|
+
parseValueBasedOnSchema: vi.fn((input, type) => {
|
|
39
|
+
if (type === 'INTEGER' && !isNaN(Number(input))) return Number(input);
|
|
40
|
+
return input;
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock('inquirer', async (importOriginal) => {
|
|
45
|
+
const original = await importOriginal();
|
|
46
|
+
return {
|
|
47
|
+
...original,
|
|
48
|
+
default: {
|
|
49
|
+
prompt: vi.fn(),
|
|
50
|
+
Separator:
|
|
51
|
+
original.Separator ||
|
|
52
|
+
class {
|
|
53
|
+
constructor() {
|
|
54
|
+
this.type = 'separator';
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
Separator:
|
|
59
|
+
original.Separator ||
|
|
60
|
+
class {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.type = 'separator';
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('actions.js', () => {
|
|
69
|
+
let db;
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
db = new Database(':memory:');
|
|
73
|
+
db.exec(`
|
|
74
|
+
CREATE TABLE users (
|
|
75
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
+
name TEXT NOT NULL,
|
|
77
|
+
age INTEGER
|
|
78
|
+
);
|
|
79
|
+
INSERT INTO users (name, age) VALUES ('Alice', 30);
|
|
80
|
+
INSERT INTO users (name, age) VALUES ('Bob', 25);
|
|
81
|
+
INSERT INTO users (name, age) VALUES ('Charlie', 35);
|
|
82
|
+
`);
|
|
83
|
+
vi.clearAllMocks();
|
|
84
|
+
if (
|
|
85
|
+
inquirerModule.default &&
|
|
86
|
+
typeof inquirerModule.default.prompt.mockClear === 'function'
|
|
87
|
+
) {
|
|
88
|
+
inquirerModule.default.prompt.mockClear();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
utilsModule.logger.error.mockClear();
|
|
92
|
+
utilsModule.logger.warn.mockClear();
|
|
93
|
+
utilsModule.logger.success.mockClear();
|
|
94
|
+
utilsModule.logger.info.mockClear();
|
|
95
|
+
utilsModule.logger.log.mockClear();
|
|
96
|
+
|
|
97
|
+
if (vi.isMockFunction(dbModule.getTableInfo)) {
|
|
98
|
+
dbModule.getTableInfo.mockRestore();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('createTable', () => {
|
|
103
|
+
it('should create a table with valid user input', async () => {
|
|
104
|
+
inquirerModule.default.prompt
|
|
105
|
+
.mockResolvedValueOnce({ tableName: 'new_users' })
|
|
106
|
+
.mockResolvedValueOnce({
|
|
107
|
+
name: 'id',
|
|
108
|
+
type: 'INTEGER',
|
|
109
|
+
pk: true,
|
|
110
|
+
dflt_val: '',
|
|
111
|
+
addMore: true,
|
|
112
|
+
})
|
|
113
|
+
.mockResolvedValueOnce({
|
|
114
|
+
name: 'email',
|
|
115
|
+
type: 'TEXT',
|
|
116
|
+
pk: false,
|
|
117
|
+
notnull: true,
|
|
118
|
+
dflt_val: '',
|
|
119
|
+
addMore: false,
|
|
120
|
+
})
|
|
121
|
+
.mockResolvedValueOnce({ confirm: true });
|
|
122
|
+
|
|
123
|
+
await createTable(db);
|
|
124
|
+
|
|
125
|
+
const tables = dbModule.getTables(db);
|
|
126
|
+
expect(tables).toContain('new_users');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('insertRow', () => {
|
|
131
|
+
it('should insert a new row with valid user input', async () => {
|
|
132
|
+
inquirerModule.default.prompt
|
|
133
|
+
.mockResolvedValueOnce({ value: 'John Doe' })
|
|
134
|
+
.mockResolvedValueOnce({ value: '30' })
|
|
135
|
+
.mockResolvedValueOnce({ ok: true });
|
|
136
|
+
|
|
137
|
+
await insertRow(db, 'users');
|
|
138
|
+
|
|
139
|
+
const stmt = db.prepare('SELECT * FROM users WHERE name = ?');
|
|
140
|
+
const user = stmt.get('John Doe');
|
|
141
|
+
|
|
142
|
+
expect(user).toBeDefined();
|
|
143
|
+
expect(user.age).toBe(30);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('updateRow', () => {
|
|
148
|
+
it('should update a specific row and column', async () => {
|
|
149
|
+
const row = dbModule.getSingleRow(db, 'users', 'id', 1);
|
|
150
|
+
inquirerModule.default.prompt
|
|
151
|
+
.mockResolvedValueOnce({ column: 'name' })
|
|
152
|
+
.mockResolvedValueOnce({ value: 'Alice Smith' })
|
|
153
|
+
.mockResolvedValueOnce({ ok: true });
|
|
154
|
+
|
|
155
|
+
await updateRow(db, 'users', 'id', row);
|
|
156
|
+
|
|
157
|
+
const updatedRow = dbModule.getSingleRow(db, 'users', 'id', 1);
|
|
158
|
+
expect(updatedRow.name).toBe('Alice Smith');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('deleteRow', () => {
|
|
163
|
+
it('should delete a specific row', async () => {
|
|
164
|
+
inquirerModule.default.prompt.mockResolvedValueOnce({ ok: true });
|
|
165
|
+
|
|
166
|
+
await deleteRow(db, 'users', 'id', 1);
|
|
167
|
+
|
|
168
|
+
const deletedRow = db.prepare('SELECT * FROM users WHERE id = 1').get();
|
|
169
|
+
expect(deletedRow).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('addColumn', () => {
|
|
174
|
+
it('should add a new column to an existing table', async () => {
|
|
175
|
+
inquirerModule.default.prompt
|
|
176
|
+
.mockResolvedValueOnce({
|
|
177
|
+
name: 'email',
|
|
178
|
+
type: 'TEXT',
|
|
179
|
+
notnull: false,
|
|
180
|
+
dflt_val: null,
|
|
181
|
+
})
|
|
182
|
+
.mockResolvedValueOnce({ confirm: true });
|
|
183
|
+
|
|
184
|
+
await addColumn(db, 'users');
|
|
185
|
+
|
|
186
|
+
const schema = dbModule.getTableInfo(db, 'users');
|
|
187
|
+
const emailColumn = schema.find((col) => col.name === 'email');
|
|
188
|
+
expect(emailColumn).toBeDefined();
|
|
189
|
+
expect(emailColumn.type).toBe('TEXT');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should add a new column with NOT NULL and default value', async () => {
|
|
193
|
+
vi.spyOn(dbModule, 'getTableInfo').mockImplementation((_db, table) => {
|
|
194
|
+
if (table === 'users') {
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
cid: 0,
|
|
198
|
+
name: 'id',
|
|
199
|
+
type: 'INTEGER',
|
|
200
|
+
notnull: 1,
|
|
201
|
+
dflt_val: null,
|
|
202
|
+
pk: 1,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
cid: 1,
|
|
206
|
+
name: 'name',
|
|
207
|
+
type: 'TEXT',
|
|
208
|
+
notnull: 1,
|
|
209
|
+
dflt_val: null,
|
|
210
|
+
pk: 0,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
cid: 2,
|
|
214
|
+
name: 'age',
|
|
215
|
+
type: 'INTEGER',
|
|
216
|
+
notnull: 0,
|
|
217
|
+
dflt_val: null,
|
|
218
|
+
pk: 0,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
cid: 3,
|
|
222
|
+
name: 'status',
|
|
223
|
+
type: 'TEXT',
|
|
224
|
+
notnull: 1,
|
|
225
|
+
dflt_val: "'active'",
|
|
226
|
+
pk: 0,
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
return dbModule.getTableInfo(_db, table);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
inquirerModule.default.prompt
|
|
234
|
+
.mockResolvedValueOnce({
|
|
235
|
+
name: 'status',
|
|
236
|
+
type: 'TEXT',
|
|
237
|
+
notnull: true,
|
|
238
|
+
dflt_val: 'active',
|
|
239
|
+
})
|
|
240
|
+
.mockResolvedValueOnce({ confirm: true });
|
|
241
|
+
|
|
242
|
+
await addColumn(db, 'users');
|
|
243
|
+
|
|
244
|
+
const schema = dbModule.getTableInfo(db, 'users');
|
|
245
|
+
const statusColumn = schema.find((col) => col.name === 'status');
|
|
246
|
+
expect(statusColumn).toBeDefined();
|
|
247
|
+
expect(statusColumn.type).toBe('TEXT');
|
|
248
|
+
expect(statusColumn.notnull).toBe(1);
|
|
249
|
+
expect(statusColumn.dflt_val).toBe("'active'");
|
|
250
|
+
|
|
251
|
+
db.exec("INSERT INTO users (name, age) VALUES ('David', 40);");
|
|
252
|
+
const newRow = db
|
|
253
|
+
.prepare("SELECT status FROM users WHERE name = 'David'")
|
|
254
|
+
.get();
|
|
255
|
+
expect(newRow.status).toBe('active');
|
|
256
|
+
|
|
257
|
+
if (vi.isMockFunction(dbModule.getTableInfo)) {
|
|
258
|
+
dbModule.getTableInfo.mockRestore();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('dropColumn', () => {
|
|
264
|
+
it('should drop an existing column from a table', async () => {
|
|
265
|
+
db.exec('ALTER TABLE users ADD COLUMN temp_col TEXT;');
|
|
266
|
+
|
|
267
|
+
inquirerModule.default.prompt
|
|
268
|
+
.mockResolvedValueOnce({ columnName: 'temp_col' })
|
|
269
|
+
.mockResolvedValueOnce({ confirm: true });
|
|
270
|
+
|
|
271
|
+
await dropColumn(db, 'users');
|
|
272
|
+
|
|
273
|
+
const schema = dbModule.getTableInfo(db, 'users');
|
|
274
|
+
const tempCol = schema.find((col) => col.name === 'temp_col');
|
|
275
|
+
expect(tempCol).toBeUndefined();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should prevent dropping the last column', async () => {
|
|
279
|
+
db.exec('CREATE TABLE single_col (col1 TEXT);');
|
|
280
|
+
|
|
281
|
+
vi.spyOn(dbModule, 'getTableInfo').mockReturnValueOnce([
|
|
282
|
+
{
|
|
283
|
+
cid: 0,
|
|
284
|
+
name: 'col1',
|
|
285
|
+
type: 'TEXT',
|
|
286
|
+
notnull: 0,
|
|
287
|
+
dflt_val: null,
|
|
288
|
+
pk: 0,
|
|
289
|
+
},
|
|
290
|
+
]);
|
|
291
|
+
i18nModule.t.mockImplementation((key) => {
|
|
292
|
+
if (key === 'common.cancel') return 'Cancel';
|
|
293
|
+
return key;
|
|
294
|
+
});
|
|
295
|
+
inquirerModule.default.prompt
|
|
296
|
+
.mockResolvedValueOnce({ columnName: 'col1' })
|
|
297
|
+
.mockResolvedValueOnce({ confirm: true });
|
|
298
|
+
|
|
299
|
+
await dropColumn(db, 'single_col');
|
|
300
|
+
|
|
301
|
+
expect(utilsModule.logger.error).toHaveBeenCalledWith(
|
|
302
|
+
'actions.dropColumnLastError'
|
|
303
|
+
);
|
|
304
|
+
const schema = dbModule.getTableInfo(db, 'single_col');
|
|
305
|
+
expect(schema).toHaveLength(1);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('renameTable', () => {
|
|
310
|
+
it('should rename an existing table', async () => {
|
|
311
|
+
inquirerModule.default.prompt
|
|
312
|
+
.mockResolvedValueOnce({ newTableName: 'people' })
|
|
313
|
+
.mockResolvedValueOnce({ confirm: true });
|
|
314
|
+
|
|
315
|
+
await renameTable(db, 'users');
|
|
316
|
+
|
|
317
|
+
const tables = dbModule.getTables(db);
|
|
318
|
+
expect(tables).not.toContain('users');
|
|
319
|
+
expect(tables).toContain('people');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('searchRows', () => {
|
|
324
|
+
it('should return rows matching the where clause', () => {
|
|
325
|
+
const results = searchRows(db, 'users', 'age > 28');
|
|
326
|
+
expect(results).toHaveLength(2);
|
|
327
|
+
expect(results[0].name).toBe('Alice');
|
|
328
|
+
expect(results[1].name).toBe('Charlie');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should return empty array if no rows match', () => {
|
|
332
|
+
const results = searchRows(db, 'users', "name = 'NonExistent'");
|
|
333
|
+
expect(results).toHaveLength(0);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('updateMultipleRows', () => {
|
|
338
|
+
it('should update multiple rows based on a where clause', async () => {
|
|
339
|
+
inquirerModule.default.prompt
|
|
340
|
+
.mockResolvedValueOnce({ column: 'age' })
|
|
341
|
+
.mockResolvedValueOnce({ value: '31' })
|
|
342
|
+
.mockResolvedValueOnce({ confirm: true });
|
|
343
|
+
|
|
344
|
+
await updateMultipleRows(
|
|
345
|
+
db,
|
|
346
|
+
'users',
|
|
347
|
+
"name = 'Alice' OR name = 'Bob'",
|
|
348
|
+
2
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const alice = db.prepare('SELECT * FROM users WHERE id = 1').get();
|
|
352
|
+
const bob = db.prepare('SELECT * FROM users WHERE id = 2').get();
|
|
353
|
+
const charlie = db.prepare('SELECT * FROM users WHERE id = 3').get();
|
|
354
|
+
|
|
355
|
+
expect(alice.age).toBe(31);
|
|
356
|
+
expect(bob.age).toBe(31);
|
|
357
|
+
expect(charlie.age).toBe(35);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should not update if user cancels', async () => {
|
|
361
|
+
inquirerModule.default.prompt
|
|
362
|
+
.mockResolvedValueOnce({ column: 'age' })
|
|
363
|
+
.mockResolvedValueOnce({ value: '99' })
|
|
364
|
+
.mockResolvedValueOnce({ confirm: false });
|
|
365
|
+
|
|
366
|
+
await updateMultipleRows(db, 'users', 'age > 20', 3);
|
|
367
|
+
|
|
368
|
+
const users = db.prepare('SELECT age FROM users').all();
|
|
369
|
+
expect(users.map((u) => u.age)).toEqual([30, 25, 35]);
|
|
370
|
+
expect(utilsModule.logger.warn).toHaveBeenCalledWith(
|
|
371
|
+
'actions.multiUpdate.cancelled'
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('deleteMultipleRows', () => {
|
|
377
|
+
it('should delete multiple rows based on a where clause', async () => {
|
|
378
|
+
inquirerModule.default.prompt.mockResolvedValueOnce({ confirm: true });
|
|
379
|
+
|
|
380
|
+
await deleteMultipleRows(db, 'users', 'age < 30', 1);
|
|
381
|
+
|
|
382
|
+
const remainingUsers = db.prepare('SELECT * FROM users').all();
|
|
383
|
+
expect(remainingUsers).toHaveLength(2);
|
|
384
|
+
expect(remainingUsers.map((u) => u.name)).not.toContain('Bob');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should not delete if user cancels', async () => {
|
|
388
|
+
inquirerModule.default.prompt.mockResolvedValueOnce({ confirm: false });
|
|
389
|
+
|
|
390
|
+
await deleteMultipleRows(db, 'users', 'age < 30', 1);
|
|
391
|
+
|
|
392
|
+
const remainingUsers = db.prepare('SELECT * FROM users').all();
|
|
393
|
+
expect(remainingUsers).toHaveLength(3);
|
|
394
|
+
expect(utilsModule.logger.warn).toHaveBeenCalledWith(
|
|
395
|
+
'actions.multiDelete.cancelled'
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import {
|
|
4
|
+
getTables,
|
|
5
|
+
getPrimaryKey,
|
|
6
|
+
getAllRows,
|
|
7
|
+
countRows,
|
|
8
|
+
getRowsPaginated,
|
|
9
|
+
getSingleRow,
|
|
10
|
+
getTableInfo,
|
|
11
|
+
executeCustomQuery,
|
|
12
|
+
} from '../db.js';
|
|
13
|
+
import * as i18nModule from '../i18n.js';
|
|
14
|
+
|
|
15
|
+
vi.mock('../i18n.js', () => ({
|
|
16
|
+
t: vi.fn((key) => key),
|
|
17
|
+
init: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('db.js', () => {
|
|
21
|
+
let db;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
if (vi.isMockFunction(i18nModule.t)) {
|
|
26
|
+
i18nModule.t.mockImplementation((key) => {
|
|
27
|
+
if (key === 'db.readOnlyError') return 'Read-only SELECT queries only.';
|
|
28
|
+
return key;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
i18nModule.init('en');
|
|
33
|
+
|
|
34
|
+
db = new Database(':memory:');
|
|
35
|
+
db.exec(`
|
|
36
|
+
CREATE TABLE users (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
name TEXT NOT NULL,
|
|
39
|
+
age INTEGER
|
|
40
|
+
);
|
|
41
|
+
`);
|
|
42
|
+
db.exec(`
|
|
43
|
+
INSERT INTO users (name, age) VALUES
|
|
44
|
+
('Alice', 30),
|
|
45
|
+
('Bob', 25),
|
|
46
|
+
('Charlie', 35);
|
|
47
|
+
`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should get all table names', () => {
|
|
51
|
+
const tables = getTables(db);
|
|
52
|
+
expect(tables).toEqual(['users']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should get the primary key of a table', () => {
|
|
56
|
+
const pk = getPrimaryKey(db, 'users');
|
|
57
|
+
expect(pk).toBe('id');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return null if no primary key exists', () => {
|
|
61
|
+
db.exec('CREATE TABLE no_pk (col1 TEXT);');
|
|
62
|
+
const pk = getPrimaryKey(db, 'no_pk');
|
|
63
|
+
expect(pk).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should get all rows from a table', () => {
|
|
67
|
+
const rows = getAllRows(db, 'users');
|
|
68
|
+
expect(rows).toHaveLength(3);
|
|
69
|
+
expect(rows[0].name).toBe('Alice');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should count the rows in a table', () => {
|
|
73
|
+
const count = countRows(db, 'users');
|
|
74
|
+
expect(count).toBe(3);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should get paginated rows', () => {
|
|
78
|
+
const rows = getRowsPaginated(db, 'users', 2, 1);
|
|
79
|
+
expect(rows).toHaveLength(2);
|
|
80
|
+
expect(rows[0].name).toBe('Bob');
|
|
81
|
+
expect(rows[1].name).toBe('Charlie');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should get a single row by its primary key', () => {
|
|
85
|
+
const row = getSingleRow(db, 'users', 'id', 2);
|
|
86
|
+
expect(row).toBeDefined();
|
|
87
|
+
expect(row.name).toBe('Bob');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should get table schema information', () => {
|
|
91
|
+
const info = getTableInfo(db, 'users');
|
|
92
|
+
expect(info).toHaveLength(3);
|
|
93
|
+
expect(info[0].name).toBe('id');
|
|
94
|
+
expect(info[0].type).toBe('INTEGER');
|
|
95
|
+
expect(info[0].pk).toBe(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('executeCustomQuery', () => {
|
|
99
|
+
it('should execute a custom SELECT query', () => {
|
|
100
|
+
const results = executeCustomQuery(
|
|
101
|
+
db,
|
|
102
|
+
'SELECT name FROM users WHERE age > 30'
|
|
103
|
+
);
|
|
104
|
+
expect(results).toHaveLength(1);
|
|
105
|
+
expect(results[0].name).toBe('Charlie');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return an empty array for SELECT queries with no results', () => {
|
|
109
|
+
const results = executeCustomQuery(
|
|
110
|
+
db,
|
|
111
|
+
'SELECT * FROM users WHERE age > 100'
|
|
112
|
+
);
|
|
113
|
+
expect(results).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should throw an error for non-SELECT custom queries', () => {
|
|
117
|
+
expect(() => executeCustomQuery(db, 'DELETE FROM users')).toThrowError(
|
|
118
|
+
'Read-only SELECT queries only.'
|
|
119
|
+
);
|
|
120
|
+
expect(() =>
|
|
121
|
+
executeCustomQuery(db, "INSERT INTO users (name) VALUES ('Dave')")
|
|
122
|
+
).toThrowError('Read-only SELECT queries only.');
|
|
123
|
+
expect(() =>
|
|
124
|
+
executeCustomQuery(db, "UPDATE users SET name = 'NewName' WHERE id = 1")
|
|
125
|
+
).toThrowError('Read-only SELECT queries only.');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
parseValueBasedOnOriginal,
|
|
4
|
+
parseValueBasedOnSchema,
|
|
5
|
+
isValidIdentifier,
|
|
6
|
+
} from '../utils.js';
|
|
7
|
+
|
|
8
|
+
describe('parseValueBasedOnOriginal', () => {
|
|
9
|
+
it('should return null for "null" string', () => {
|
|
10
|
+
expect(parseValueBasedOnOriginal('null', '')).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return a number for a string number if original is a number', () => {
|
|
14
|
+
expect(parseValueBasedOnOriginal('123', 0)).toBe(123);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return undefined for an invalid number string if original is a number', () => {
|
|
18
|
+
expect(parseValueBasedOnOriginal('abc', 0)).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should return a BigInt for a string number if original is a bigint', () => {
|
|
22
|
+
expect(parseValueBasedOnOriginal('123', BigInt(0))).toBe(BigInt(123));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return the original string for other cases', () => {
|
|
26
|
+
expect(parseValueBasedOnOriginal('hello', 'world')).toBe('hello');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return an empty string if input is empty and original is not null', () => {
|
|
30
|
+
expect(parseValueBasedOnOriginal('', 'world')).toBe('');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('parseValueBasedOnSchema', () => {
|
|
35
|
+
it('should return null for "null" string', () => {
|
|
36
|
+
expect(parseValueBasedOnSchema('null', 'TEXT')).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return null for an empty string', () => {
|
|
40
|
+
expect(parseValueBasedOnSchema('', 'TEXT')).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return a number for an INT type', () => {
|
|
44
|
+
expect(parseValueBasedOnSchema('42', 'INTEGER')).toBe(42);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return a number for a REAL type', () => {
|
|
48
|
+
expect(parseValueBasedOnSchema('3.14', 'REAL')).toBe(3.14);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return undefined for an invalid number for INT type', () => {
|
|
52
|
+
expect(parseValueBasedOnSchema('abc', 'INTEGER')).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return the original string for a TEXT type', () => {
|
|
56
|
+
expect(parseValueBasedOnSchema('hello world', 'TEXT')).toBe('hello world');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('isValidIdentifier', () => {
|
|
61
|
+
it('should return true for valid identifiers', () => {
|
|
62
|
+
expect(isValidIdentifier('my_table')).toBe(true);
|
|
63
|
+
expect(isValidIdentifier('column1')).toBe(true);
|
|
64
|
+
expect(isValidIdentifier('my-table')).toBe(true);
|
|
65
|
+
expect(isValidIdentifier('my table')).toBe(true);
|
|
66
|
+
expect(isValidIdentifier('version1.2')).toBe(true);
|
|
67
|
+
expect(isValidIdentifier('users_v2')).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return false for invalid identifiers', () => {
|
|
71
|
+
expect(isValidIdentifier('')).toBe(false);
|
|
72
|
+
expect(isValidIdentifier(' ')).toBe(false);
|
|
73
|
+
expect(isValidIdentifier('my"table')).toBe(false);
|
|
74
|
+
expect(isValidIdentifier('my`table')).toBe(false);
|
|
75
|
+
expect(isValidIdentifier("my'table")).toBe(false);
|
|
76
|
+
expect(isValidIdentifier('my/table')).toBe(false);
|
|
77
|
+
expect(isValidIdentifier(null)).toBe(false);
|
|
78
|
+
expect(isValidIdentifier(undefined)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|