@intecoag/inteco-cli 1.9.0 → 1.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intecoag/inteco-cli",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "CLI-Tools for Inteco",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ import showChangelog from './modules/changelog.js';
17
17
  import commands from "./ressources/cmds.json" with {type: 'json'};
18
18
  import packageJson from "../package.json" with {type: 'json'}
19
19
  import extdSearch from './modules/extdSearch.js';
20
+ import t009Search from './modules/t009Search.js';
20
21
  import syncConfig from './modules/syncConfig.js';
21
22
  import configMutation from './modules/configMutation.js';
22
23
  import bundleProduct from './modules/bundleProduct.js';
@@ -89,6 +90,9 @@ switch (cli.input[0]) {
89
90
  case "extd_search":
90
91
  extdSearch();
91
92
  break;
93
+ case "t009_search":
94
+ t009Search();
95
+ break;
92
96
  case "sync_config":
93
97
  syncConfig();
94
98
  break;
@@ -1,200 +1,85 @@
1
- import { DB } from "../utils/db/DB.js";
2
- import prompts from "prompts";
3
- import CliTable3 from "cli-table3";
4
- import readline from "readline";
5
- import chalk from "chalk";
6
- import fuzzysort from "fuzzysort";
1
+ import { DatabaseShellBuilder, TableConfig } from "../utils/shell/DatabaseShellBuilder.js";
7
2
 
8
3
  // Entry point
9
4
  export default async function extdSearch() {
10
- const responses = await configureDB();
11
-
12
- if (!responses.dbName) return;
13
-
14
- let currentSearchType = configs[0];
15
- let lastQuery = '';
16
-
17
- const baseData = await loadData(responses.dbName, responses.tables);
18
- const rl = createReadline();
19
-
20
- console.log(chalk.green("Interactive Search Started."));
21
- rl.prompt();
22
-
23
- rl.on('line', async (input) => {
24
- const query = input.trim();
25
-
26
- if (handleBuiltInCommands(query, rl)) return;
27
-
28
- const matchedConfig = configs.find(c => query === `:${c.cmd}`);
29
- if (matchedConfig) {
30
- currentSearchType = matchedConfig;
31
- } else {
32
- lastQuery = query;
33
- }
34
-
35
- const results = await fuzzySearch(baseData, lastQuery, currentSearchType);
36
- renderTable(results, currentSearchType);
37
- rl.prompt();
38
- });
39
-
40
- rl.on('close', () => {
41
- console.log(chalk.yellow("Search session ended."));
42
- process.exit(0);
43
- });
44
- }
45
-
46
- function createReadline() {
47
- return readline.createInterface({
48
- input: process.stdin,
49
- output: process.stdout,
50
- prompt: 'Search query (show help with :?)> '
51
- });
52
- }
53
-
54
- function handleBuiltInCommands(query, rl) {
55
- switch (query) {
56
- case ':q':
57
- rl.close();
58
- return true;
59
- case ':?':
60
- printHelp();
61
- rl.prompt();
62
- return true;
63
- default:
64
- return false;
65
- }
66
- }
67
-
68
- function printHelp() {
69
- console.log("\n" + chalk.cyan("Available Commands:"));
70
- console.log(chalk.yellow(":q") + " - Exit the application");
71
- console.log(chalk.yellow(":?") + " - Show this help");
72
- configs.forEach(config =>
73
- console.log(`${chalk.yellow(`:${config.cmd}`)} - Switch to ${config.name}`)
74
- );
75
- console.log();
76
- }
77
-
78
- async function configureDB() {
79
- console.log();
80
- const databaseNames = await DB.getDatabaseNames();
81
-
82
- return await prompts([
83
- {
84
- type: 'autocomplete',
85
- name: 'dbName',
86
- message: 'DB-Name?',
87
- choices: databaseNames.map(db => ({ title: db.name }))
88
- },
89
- {
90
- type: 'select',
91
- name: 'tables',
92
- message: 'Search-Type?',
93
- choices: [
94
- { title: 'EXTD/EXTI', value: 'EXTD/EXTI' },
95
- { title: 'EXTI only', value: 'EXTI' },
96
- { title: 'EXTD only', value: 'EXTD' }
97
- ]
98
- }
99
- ], {
100
- onCancel: () => {
101
- console.log("\n" + chalk.red("Cancelled Search!\n"));
102
- }
103
- });
104
- }
105
-
106
- async function loadData(db, tableChoice) {
107
- const extd = await getTableData('extd', db);
108
- const exti = await getTableData('exti', db);
109
-
110
- const cleaned = [
111
- ...cleanData(extd, 'extd'),
112
- ...cleanData(exti, 'exti')
113
- ];
114
-
115
- if (tableChoice === 'EXTD') return cleaned.filter(e => e.table === 'extd');
116
- if (tableChoice === 'EXTI') return cleaned.filter(e => e.table === 'exti');
117
- return cleaned;
118
- }
119
-
120
- function cleanData(entries, tableName) {
121
- return entries.map(entry => {
122
- const stripped = Object.fromEntries(Object.entries(entry).map(([k, v]) => {
123
- return [k.replace(/^ext[di]_/, ''), v];
124
- }));
125
- return { ...stripped, table: tableName };
126
- });
127
- }
128
-
129
- async function getTableData(table, db) {
130
- const query = `SELECT * FROM ${table};`;
131
- return await DB.executeQueryOnDB(query, db);
132
- }
133
-
134
- async function fuzzySearch(data, query, config) {
135
- if (!query) return data.slice(0, 1000);
136
- return fuzzysort.go(query, data, {
137
- keys: config.searchKeys,
138
- limit: 1000,
139
- threshold: config.threshold
140
- }).map(r => ({ ...r.obj, score: r.score }));
141
- }
142
-
143
- function renderTable(data, config) {
144
- const table = new CliTable3({ head: config.tableHeader });
145
- data.forEach(row => table.push(config.tableFormatter(row)));
146
- console.log(table.toString());
147
- }
148
-
149
- // ---------- Configurations & Helpers ----------
150
-
151
- const configs = [
152
- {
153
- name: "Overview",
154
- cmd: "ow",
155
- searchKeys: ['mnr', 'name', 'bez_d', 'bez_f', 'bez_i'],
156
- threshold: 0.7,
157
- tableHeader: ['MNR', 'Table', 'Name', 'Bezeichnung Deutsch', 'Bezeichnung Französisch', 'Bezeichnung Italieniesch'],
158
- tableFormatter: p => [p.mnr, p.table, p.name, p.bez_d, p.bez_f, p.bez_i]
159
- },
160
- {
161
- name: "Field-Information",
162
- cmd: "fi",
163
- searchKeys: [
5
+ const overviewConfig = new TableConfig()
6
+ .withName("Overview")
7
+ .withShortcut(":ow")
8
+ .withSearchKeys(["mnr", "name", "bez_d", "bez_f", "bez_i"])
9
+ .withThreshold(0.7)
10
+ .withHeader(["MNR", "Table", "Name", "Bezeichnung Deutsch", "Bezeichnung Französisch", "Bezeichnung Italienisch"])
11
+ .withFormatter(p => [p.mnr, p.table, p.name, p.bez_d, p.bez_f, p.bez_i]);
12
+
13
+ const fieldInfoConfig = new TableConfig()
14
+ .withName("Field-Information")
15
+ .withShortcut(":fi")
16
+ .withSearchKeys([
164
17
  'name', 'bez_d', 'bez_f', 'bez_i',
165
18
  'b_dtext_1', 'b_dtext_2', 'b_dtext_3',
166
19
  'b_dtext_4', 'b_dtext_5', 'b_dtext_6',
167
20
  'b_dtext_7', 'b_dtext_8', 'b_dtext_9'
168
- ],
169
- threshold: 0.7,
170
- tableHeader: ['MNR', 'Table', 'Name', 'Feldtyp', 'Flag', 'Testflag', 'Wert 1', 'Wert 2', 'Wert 3', 'Wert 4', 'Wert 5', 'Wert 6', 'Wert 7', 'Wert 8', 'Wert 9'],
171
- tableFormatter: p => [
21
+ ])
22
+ .withThreshold(0.7)
23
+ .withHeader(['MNR', 'Table', 'Name', 'Feldtyp', 'Flag', 'Testflag', 'Wert 1', 'Wert 2', 'Wert 3', 'Wert 4', 'Wert 5', 'Wert 6', 'Wert 7', 'Wert 8', 'Wert 9'])
24
+ .withFormatter(p => [
172
25
  p.mnr, p.table, p.name,
173
26
  formatFieldType(p.special),
174
27
  formatFlag(p.flag),
175
- formatTestFlag(p.testflag),
28
+ formatTestFlag(p.testflab),
176
29
  ...Array.from({ length: 9 }, (_, i) => formatWert(p[`b_value_${i + 1}`], p[`b_dtext_${i + 1}`]))
177
- ]
178
- },
179
- {
180
- name: "Disp-Fields",
181
- cmd: "df",
182
- searchKeys: [
30
+ ]);
31
+
32
+ const dispFieldsConfig = new TableConfig()
33
+ .withName("Disp-Fields")
34
+ .withShortcut(":df")
35
+ .withSearchKeys([
183
36
  'mnr', 'name', 'bez_d', 'testfeld',
184
37
  'disp_feld_1', 'disp_feld_2', 'disp_feld_3',
185
38
  'disp_feld_4', 'disp_feld_5', 'disp_feld_6',
186
39
  'disp_feld_7', 'disp_feld_8', 'disp_feld_9'
187
- ],
188
- threshold: 0.7,
189
- tableHeader: ['MNR', 'Table', 'Name', 'Test-Feld', 'Dispmask', 'Dispfeld 1', 'Dispfeld 2', 'Dispfeld 3', 'Dispfeld 4', 'Dispfeld 5', 'Dispfeld 6', 'Dispfeld 7', 'Dispfeld 8', 'Dispfeld 9'],
190
- tableFormatter: p => [
40
+ ])
41
+ .withThreshold(0.7)
42
+ .withHeader(['MNR', 'Table', 'Name', 'Test-Feld', 'Dispmask', 'Dispfeld 1', 'Dispfeld 2', 'Dispfeld 3', 'Dispfeld 4', 'Dispfeld 5', 'Dispfeld 6', 'Dispfeld 7', 'Dispfeld 8', 'Dispfeld 9'])
43
+ .withFormatter(p => [
191
44
  p.mnr, p.table, p.name, p.testfeld, p.dispmask,
192
45
  p.disp_feld_1, p.disp_feld_2, p.disp_feld_3,
193
46
  p.disp_feld_4, p.disp_feld_5, p.disp_feld_6,
194
47
  p.disp_feld_7, p.disp_feld_8, p.disp_feld_9
195
- ]
196
- }
197
- ];
48
+ ]);
49
+
50
+ const builder = new DatabaseShellBuilder();
51
+ builder
52
+ .withEditTools(
53
+ [
54
+ {
55
+ title: 'MNR', column: 'mnr', default: '1'
56
+ },
57
+ {
58
+ title: 'Name', column: 'name', default: null,
59
+ },
60
+ {
61
+ title: 'Bezeichnung Deutsch', column: 'bez_d', default: null,
62
+ },
63
+ {
64
+ title: 'Bezeichnung Französisch', column: 'bez_f', default: null,
65
+ },
66
+ {
67
+ title: 'Bezeichnung Italienisch', column: 'bez_i', default: null
68
+ }
69
+ ],
70
+ ["name"],
71
+ ["name"],
72
+ ["mnr", "bez_d", "bez_f", "bez_i"])
73
+ .withConfig(overviewConfig)
74
+ .withConfig(fieldInfoConfig)
75
+ .withConfig(dispFieldsConfig)
76
+ .withTables("EXTD/EXTI", ["extd", "exti"])
77
+ .withTables("EXTD", ["extd"])
78
+ .withTables("EXTI", ["exti"]);
79
+
80
+ const shell = builder.build();
81
+ await shell.run();
82
+ }
198
83
 
199
84
  function formatWert(value, bez) {
200
85
  return value || bez ? `'${value}'='${bez}'` : '';
@@ -0,0 +1,34 @@
1
+ import { DatabaseShellBuilder, TableConfig } from "../utils/shell/DatabaseShellBuilder.js";
2
+
3
+ export default async function t009Search() {
4
+
5
+ const overviewConfig = new TableConfig()
6
+ .withName("Overview")
7
+ .withShortcut(":ow")
8
+ .withSearchKeys(["mnr", "grp_pw", "lnr", "pgm", "flag", "bez", "bez_f", "bez_i"])
9
+ .withThreshold(0.7)
10
+ .withHeader(["MNR", "Mitarbeitergruppe", "Zeilennummer", "Programm-Name", "Typ (P=Programm, S=Shell)", "Bezeichnung", "Bezeichnung franz.", "Bezeichnung ital."])
11
+ .withFormatter(p => [p.mnr, p.grp_pw, p.lnr, p.pgm, p.flag, p.bez, p.bez_f, p.bez_i]);
12
+
13
+ const builder = new DatabaseShellBuilder();
14
+ builder
15
+ .withEditTools(
16
+ [
17
+ { title: 'MNR', column: 'mnr', default: '1' },
18
+ { title: 'Mitarbeitergruppe', column: 'grp_pw', default: '' },
19
+ { title: 'Zeilennummer', column: 'lnr', default: null },
20
+ { title: 'Programm-Name', column: 'pgm', default: null },
21
+ { title: 'Typ', column: 'flag', default: 'P' },
22
+ { title: 'Bezeichnung', column: 'bez', default: null },
23
+ { title: 'Bezeichnung franz.', column: 'bez_f', default: null },
24
+ { title: 'Bezeichnung ital.', column: 'bez_i', default: null },
25
+ ],
26
+ ["mnr", "pgm"],
27
+ ["mnr", "lnr", "pgm", "bez"],
28
+ ["grp_pw", "flag", "bez_f", "bez_i"])
29
+ .withConfig(overviewConfig)
30
+ .withTables("T009", ["t009"]);
31
+
32
+ const shell = builder.build();
33
+ await shell.run();
34
+ }
@@ -29,6 +29,9 @@
29
29
  "extd_search": {
30
30
  "desc": "Search EXTD/EXTI interactively with full text search"
31
31
  },
32
+ "t009_search": {
33
+ "desc": "Search t009 interactively with full text search"
34
+ },
32
35
  "set_cli_config": {
33
36
  "desc": "Configure the Inteco CLI"
34
37
  },
Binary file
@@ -0,0 +1,260 @@
1
+ import chalk from "chalk";
2
+ import { Shell } from "./shell.js";
3
+ import { DB } from "../db/DB.js";
4
+ import fuzzysort from "fuzzysort";
5
+ import CliTable3 from "cli-table3";
6
+ import prompts from "prompts";
7
+ import { TableConfig } from "./DatabaseShellBuilder.js";
8
+
9
+ export class DatabaseShell {
10
+
11
+ constructor() {
12
+ /**
13
+ * The currently active tables to load the data from
14
+ * @type {string[]}
15
+ * @private
16
+ */
17
+ this.currentTables = [];
18
+
19
+ /**
20
+ * The currently active config
21
+ * @type {TableConfig}
22
+ */
23
+ this.currentConfig = null;
24
+
25
+ /**
26
+ * The active database
27
+ * @type {string}
28
+ * @private
29
+ */
30
+ this.db = null;
31
+
32
+ /**
33
+ * The cached data of the active table
34
+ * @type {object[]}
35
+ * @private
36
+ */
37
+ this.data = [];
38
+
39
+ /**
40
+ * The tables the user can select to use
41
+ * @type {object[]}
42
+ */
43
+ this.tables = [];
44
+
45
+ /**
46
+ * The underlying shell to handle user input
47
+ * @type {Shell}
48
+ */
49
+ this.shell = null;
50
+ }
51
+
52
+ /**
53
+ * Configures the database shell and runs the underlying shell
54
+ */
55
+ async run() {
56
+ await this.configure();
57
+ await this.shell.run();
58
+ }
59
+
60
+ /**
61
+ * Searches for a string based on the current shell state and prints the results to the console.
62
+ * @param {string} query The search string
63
+ */
64
+ executeSearch(query) {
65
+ const results = this.fuzzySearch(query);
66
+ this.renderTable(results);
67
+ }
68
+
69
+ async deleteRow(row) {
70
+ const table = row.table;
71
+ const where = Object.entries(row).filter(([k, v]) => k !== "table").map(([k, v]) => `\`${table}_${k}\`='${String(v).replaceAll("'", "\\'")}'`).join(' AND ');
72
+ const sql = `DELETE FROM \`${table}\` WHERE ${where}`;
73
+ await DB.executeQueryOnDB(sql, this.db);
74
+ await this.loadData();
75
+ }
76
+
77
+ async insertRow(row) {
78
+ const table = row.table;
79
+ const kvPairs = Object.entries(row).filter(([k, v]) => k !== "table");
80
+ const sql = `INSERT INTO \`${table}\` (${kvPairs.map(([k, v]) => `\`${table}_${k}\``).join(', ')}) VALUES (${kvPairs.map(([k, v]) => `'${String(v).replaceAll("'", "\\'")}'`)});`;
81
+ await DB.executeQueryOnDB(sql, this.db);
82
+ await this.loadData();
83
+ }
84
+
85
+ async updateRow(row, whereRow) {
86
+ const table = whereRow.table;
87
+ const set = Object.entries(row).filter(([k, v]) => k !== "table").map(([k, v]) => `\`${table}_${k}\`='${String(v).replaceAll("'", "\\'")}'`).join(', ');
88
+ const where = Object.entries(whereRow).filter(([k, v]) => k !== "table").map(([k, v]) => `\`${table}_${k}\`='${String(v).replaceAll("'", "\\'")}'`).join(' AND ');
89
+ const sql = `UPDATE \`${table}\` SET ${set} WHERE ${where};`;
90
+ await DB.executeQueryOnDB(sql, this.db);
91
+ await this.loadData();
92
+ }
93
+
94
+ async editRow(data, columns, requiredFields, optionalFields) {
95
+ let cancelled = false;
96
+ const questions = [...requiredFields, ...optionalFields]
97
+ .filter(f => Object.hasOwn(data, f))
98
+ .map(f => ({
99
+ type: 'text',
100
+ message: columns.find(c => c.column === f).title,
101
+ name: f,
102
+ initial: columns.find(c => c.column === f).default ?? data[f]}));
103
+
104
+ const responses = await prompts(questions, { onCancel: () => { cancelled = true; return false; } });
105
+
106
+ if(cancelled) return null;
107
+
108
+ const newData = {
109
+ ...data,
110
+ ...responses
111
+ };
112
+
113
+
114
+ return newData;
115
+ }
116
+
117
+ async selectRow(columns, requiredColumns) {
118
+ let cancelled = false;
119
+ const questions = requiredColumns.map(f => ({
120
+ type: 'text',
121
+ message: columns.find(c => c.column === f).title,
122
+ name: f,
123
+ initial: columns.find(c => c.column === f).default
124
+ }));
125
+
126
+ const responses = await prompts(questions, { onCancel: () => { cancelled = true; return false; } });
127
+ if(cancelled) return null;
128
+ const results = this.data.filter(d => Object.entries(responses).every(([k, v]) => d[k] == v));
129
+
130
+ if(results.length == 0) {
131
+ console.log(chalk.red("No results found."));
132
+ return null;
133
+ }
134
+
135
+ if(results.length > 1) {
136
+ console.log(chalk.red("Multiple results found. Please select one from the list..."));
137
+ const selection = await prompts({
138
+ type: 'autocomplete',
139
+ choices: results.map(r => ({
140
+ title: Object.entries(r)
141
+ .filter(([k, v]) => k === "table" || columns.some(c => c.column === k))
142
+ .map(([k, v]) => `${k === "table" ? "Table" : columns.find(c => c.column === k).title}: ${v}`)
143
+ .join(", "),
144
+ value: r
145
+ })),
146
+ message: "Select Row",
147
+ name: 'result'
148
+ }, { onCancel: () => { cancelled = true; return false; } });
149
+
150
+ if(cancelled) return null;
151
+
152
+ return selection.result;
153
+ }
154
+
155
+ return results[0];
156
+ }
157
+
158
+ /**
159
+ * Loads the rows from the currently active tables into the shell
160
+ * @private
161
+ */
162
+ async loadData() {
163
+ const data = [];
164
+
165
+ for(let i = 0; i < this.currentTables.length; i++) {
166
+ const tableData = await this.getTableData(this.currentTables[i]);
167
+ data.push(...this.cleanData(tableData, this.currentTables[i]));
168
+ }
169
+
170
+ this.data = data;
171
+ }
172
+
173
+ /**
174
+ * Loads all rows from a table of the currently active database
175
+ * @param {string} table The table name
176
+ * @returns {Promise<any>}
177
+ * @private
178
+ */
179
+ async getTableData(table) {
180
+ const query = `SELECT * FROM ${table};`;
181
+ return await DB.executeQueryOnDB(query, this.db);
182
+ }
183
+
184
+ /**
185
+ * Removes the table name prefix from the individual rows
186
+ * @param {object[]} entries The rows of the table
187
+ * @param {string} tableName The name of the table where the data is from
188
+ * @returns {object[]}
189
+ * @private
190
+ */
191
+ cleanData(entries, tableName) {
192
+ return entries.map(entry => {
193
+ const stripped = Object.fromEntries(
194
+ Object.entries(entry).map(([k, v]) => [
195
+ k.replace(new RegExp(`^${tableName}_`), ''),
196
+ v
197
+ ])
198
+ );
199
+
200
+ return { ...stripped, table: tableName };
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Executes a query on the currently loaded table using fuzzysearch
206
+ * @param {string} query The search query
207
+ * @returns {object[]}
208
+ * @private
209
+ */
210
+ fuzzySearch(query) {
211
+ if(!query) return this.data.slice(0, 1000);
212
+
213
+ return fuzzysort.go(query, this.data, {
214
+ keys: this.currentConfig.searchKeys,
215
+ limit: 1000,
216
+ threshold: this.currentConfig.threshold
217
+ }).map(r => ({ ...r.obj, score: r.score }));
218
+ }
219
+
220
+ /**
221
+ * Renders a table in the console using the currently active config
222
+ * @param {object[]} tableData The table data to render
223
+ * @private
224
+ */
225
+ renderTable(tableData) {
226
+ const table = new CliTable3({ head: this.currentConfig.tableHeader });
227
+ tableData.forEach(row => {
228
+ table.push(this.currentConfig.tableFormatter(row));
229
+ });
230
+ console.log(table.toString());
231
+ }
232
+
233
+ /**
234
+ * Prompts the user for the database and search type
235
+ */
236
+ async configure() {
237
+ console.log();
238
+ const databaseNames = await DB.getDatabaseNames();
239
+
240
+ const results = await prompts([
241
+ {
242
+ type: 'autocomplete',
243
+ name: 'dbName',
244
+ message: 'DB-Name?',
245
+ choices: databaseNames.map(db => ({ title: db.name }))
246
+ },
247
+ {
248
+ type: 'select',
249
+ name: 'tables',
250
+ message: 'Search-Type?',
251
+ choices: this.tables.map(t => ({ title: t.name, value: t.tables }))
252
+ }
253
+ ]);
254
+
255
+ this.db = results.dbName;
256
+ this.currentTables = results.tables;
257
+
258
+ await this.loadData();
259
+ }
260
+ }
@@ -0,0 +1,170 @@
1
+ import chalk from "chalk";
2
+ import { Shell } from "./shell.js";
3
+ import { ShellBuilder } from "./ShellBuilder.js"
4
+ import { DatabaseShell } from "./DatabaseShell.js";
5
+
6
+ export class DatabaseShellBuilder {
7
+
8
+ constructor() {
9
+ /**
10
+ * The available configs for the shell
11
+ * @type {TableConfig}
12
+ * @private
13
+ */
14
+ this.configs = [];
15
+
16
+ /**
17
+ * The available tables for the shell
18
+ * @type {object[]}
19
+ * @private
20
+ */
21
+ this.tables = [];
22
+
23
+ this.editTools = null;
24
+ }
25
+
26
+ /**
27
+ * Adds a table config to the shell
28
+ * @param {TableConfig} tableConfig The config to add
29
+ * @returns {DatabaseShellBuilder}
30
+ */
31
+ withConfig(tableConfig) {
32
+ this.configs.push(tableConfig);
33
+ return this;
34
+ }
35
+
36
+ /**
37
+ * Adds a source table to the shell
38
+ * @param {string} name The name displayed in the selection
39
+ * @param {string[]} tables The names of the tables where to load the data from
40
+ * @returns {DatabaseShellBuilder}
41
+ */
42
+ withTables(name, tables) {
43
+ this.tables.push({ name: name, tables: tables });
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Enables edit tools with the specified selectors and values
49
+ * @param {{title: string, column: string, default: string}[]} columns The colums that are used in the editor
50
+ * @param {string[]} requiredSelectors The columns that are required to be filled in to select an entry to edit
51
+ * @param {string[]} requiredValues The columns that are required to be filled to update an entry
52
+ * @param {string[]} optionalValues The columns that can be optionally updated
53
+ * @returns {DatabaseShellBuilder}
54
+ */
55
+ withEditTools(columns, requiredSelectors, requiredValues, optionalValues) {
56
+ this.editTools = {
57
+ columns: columns,
58
+ requiredSelectors: requiredSelectors,
59
+ requiredValues: requiredValues,
60
+ optionalValues: optionalValues
61
+ };
62
+
63
+ return this;
64
+ }
65
+
66
+ /**
67
+ * Builds the Shell
68
+ * @returns {DatabaseShell}
69
+ */
70
+ build() {
71
+ const databaseShell = new DatabaseShell();
72
+
73
+ const builder = new ShellBuilder();
74
+ builder.withBuiltInHandler()
75
+ .withBuiltInCommands()
76
+ .withCommandHandler((_, input) => { databaseShell.executeSearch(input); return true; })
77
+ .withCommand(":cfg", "Reconfigure the Database and Search Type", async () => await databaseShell.configure());
78
+
79
+ this.configs.forEach(config => {
80
+ builder.withCommand(config.cmdShortcut, `Switch to ${config.name}`, () => {
81
+ databaseShell.currentConfig = config;
82
+ console.log(`Switched to ${chalk.yellow(config.name)}.`);
83
+ });
84
+ });
85
+
86
+ if(this.editTools !== null) {
87
+ builder.withCommand(":del", "Delete a row", async (shell, input, args) => {
88
+ console.log(chalk.yellow("Select the row to delete: "));
89
+ const rowData = await databaseShell.selectRow(this.editTools.columns, this.editTools.requiredSelectors);
90
+ if(!rowData) return;
91
+
92
+ await databaseShell.deleteRow(rowData);
93
+ console.log(chalk.red("Row deleted"));
94
+ })
95
+ .withCommand(":dup", "Duplicate a row", async (shell, input, args) => {
96
+ console.log(chalk.yellow("Select the row to duplicate: "));
97
+ const rowData = await databaseShell.selectRow(this.editTools.columns, this.editTools.requiredSelectors);
98
+ if(!rowData) return;
99
+
100
+ console.log(chalk.yellow("Enter the updated row data: "));
101
+ const editedData = await databaseShell.editRow(rowData, this.editTools.columns, this.editTools.requiredValues, this.editTools.optionalValues);
102
+ if(!editedData) return;
103
+
104
+ await databaseShell.insertRow(editedData);
105
+ console.log(chalk.green("Row duplicated"));
106
+ })
107
+ .withCommand(":mod", "Modify an existing row", async (shell, input, args) => {
108
+ console.log(chalk.yellow("Select the row to edit: "));
109
+ const rowData = await databaseShell.selectRow(this.editTools.columns, this.editTools.requiredSelectors);
110
+ if(rowData === null) return;
111
+
112
+ console.log(chalk.yellow("Enter the updated row data: "));
113
+ const editedData = await databaseShell.editRow(rowData, this.editTools.columns, this.editTools.requiredValues, this.editTools.optionalValues);
114
+ if(!editedData) return;
115
+
116
+ await databaseShell.updateRow(editedData, rowData);
117
+ console.log(chalk.yellow("Row updated"));
118
+ });
119
+ }
120
+
121
+ const shell = builder.build();
122
+
123
+ databaseShell.shell = shell;
124
+ databaseShell.currentConfig = this.configs[0];
125
+ databaseShell.tables = this.tables;
126
+
127
+ return databaseShell;
128
+ }
129
+ }
130
+
131
+ export class TableConfig {
132
+ constructor() {
133
+ this.name = "";
134
+ this.cmdShortcut = "";
135
+ this.searchKeys = [];
136
+ this.threshold = 0.7;
137
+ this.tableHeader = [];
138
+ this.tableFormatter = () => [];
139
+ }
140
+
141
+ withName(name) {
142
+ this.name = name;
143
+ return this;
144
+ }
145
+
146
+ withShortcut(shortcut) {
147
+ this.cmdShortcut = shortcut;
148
+ return this;
149
+ }
150
+
151
+ withSearchKeys(keys) {
152
+ this.searchKeys = keys;
153
+ return this;
154
+ }
155
+
156
+ withThreshold(threshold) {
157
+ this.threshold = threshold;
158
+ return this;
159
+ }
160
+
161
+ withHeader(tableHeader) {
162
+ this.tableHeader = tableHeader;
163
+ return this;
164
+ }
165
+
166
+ withFormatter(formatter) {
167
+ this.tableFormatter = formatter;
168
+ return this;
169
+ }
170
+ }
@@ -0,0 +1,82 @@
1
+ import chalk from "chalk";
2
+ import prompts from "prompts";
3
+
4
+ export class Shell {
5
+
6
+ constructor() {
7
+ this.commands = [];
8
+ this.commandHandlers = [];
9
+ this.name = "";
10
+ this.prompt = "";
11
+ this.autocomplete = [];
12
+
13
+ this.exitRequested = false;
14
+ }
15
+
16
+ /**
17
+ * Starts the shell in a loop and continues until exit() is called.
18
+ */
19
+ async run() {
20
+ this.exitRequested = false;
21
+
22
+ console.log(chalk.green(`${this.name} Started.`));
23
+
24
+ while (!this.exitRequested) {
25
+ const response = await prompts({
26
+ type: "autocomplete",
27
+ name: "input",
28
+ message: this.prompt,
29
+ choices: this.autocomplete.map(ac => ({
30
+ title: ac,
31
+ value: ac
32
+ })),
33
+ limit: 1,
34
+ suggest: async (input, choices) => {
35
+ const text = input ?? "";
36
+ const q = text.trim().toLowerCase();
37
+
38
+ const matches = q.length === 0
39
+ ? choices
40
+ : choices.filter(choice => choice.title.toLowerCase().startsWith(q));
41
+
42
+ return [
43
+ {
44
+ title: text.length > 0 ? text : " ",
45
+ value: text
46
+ },
47
+ ...matches.filter(choice => choice.value !== text)
48
+ ];
49
+ }
50
+ });
51
+
52
+ const input = (response?.input ?? "").trim();
53
+
54
+ if (!await this.handleCommand(input)) {
55
+ console.log(chalk.red(`Unknown command '${input}'`));
56
+ }
57
+ }
58
+
59
+ console.log(chalk.yellow(`\n\n${this.name} Ended.`));
60
+ }
61
+
62
+ /**
63
+ * Requests the shell to exit
64
+ */
65
+ exit() {
66
+ this.exitRequested = true;
67
+ }
68
+
69
+ /**
70
+ * Executes a single command in the shell
71
+ * @param {String} input The command to execute
72
+ * @returns {Promise<boolean>}
73
+ */
74
+ async handleCommand(input) {
75
+ for(let i = 0; i < this.commandHandlers.length; i++) {
76
+ const handler = this.commandHandlers[i];
77
+ if(await handler(this, input))
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+ }
@@ -0,0 +1,177 @@
1
+ import chalk from "chalk";
2
+ import { Shell } from "./shell.js";
3
+
4
+ export class ShellBuilder {
5
+
6
+ constructor() {
7
+ this.commands = [];
8
+ this.commandHandlers = [];
9
+ this.name = "Interactive Shell";
10
+ this.prompt = "Interactive Shell (show help with :?)";
11
+ this.useBuiltInHandler = false;
12
+ }
13
+
14
+ /**
15
+ * Adds the built-in command handler
16
+ * @returns {ShellBuilder}
17
+ */
18
+ withBuiltInHandler() {
19
+ this.withCommandHandler(async (shell, input) => {
20
+ const parts = this.parseArgs(input);
21
+ if(parts.length == 0) return false;
22
+
23
+ const commandName = parts[0];
24
+ const args = parts.slice(1);
25
+
26
+ for(let i = 0; i < shell.commands.length; i++) {
27
+ const command = shell.commands[i];
28
+
29
+ if(command.name == commandName) {
30
+ await command.handler(shell, input, ...args);
31
+ return true;
32
+ }
33
+ }
34
+
35
+ return false;
36
+ });
37
+
38
+ this.useBuiltInHandler = true;
39
+
40
+ return this;
41
+ }
42
+
43
+ /**
44
+ * Adds the built-in commands (:q and :?), requires a supported handler to work
45
+ * @returns {ShellBuilder}
46
+ */
47
+ withBuiltInCommands() {
48
+ this.withCommand(":q", "Exit the shell", shell => {
49
+ shell.exit();
50
+ })
51
+ .withCommand(":?", "Shows this help", shell => {
52
+ console.log("\n" + chalk.cyan("Available Commands:"));
53
+
54
+ shell.commands.forEach(command => {
55
+ console.log(`${chalk.yellow(command.name)} - ${command.helpText}`);
56
+ });
57
+ });
58
+
59
+ return this;
60
+ }
61
+
62
+ /**
63
+ * Add a command to be executed by the built in command handler
64
+ * @param {String} commandName The command name to be entered in the command line
65
+ * @param {String} helpText The text displayed in the help
66
+ * @param {(shell: Shell, input: String, ...args: String[]) => void} handler The handler to be executed with the executing shell passed as an arugment
67
+ * @returns {ShellBuilder}
68
+ */
69
+ withCommand(commandName, helpText, handler) {
70
+ this.commands.push({
71
+ name: commandName,
72
+ handler: handler,
73
+ helpText: helpText
74
+ });
75
+ return this;
76
+ }
77
+
78
+ /**
79
+ * Change the name of the shell
80
+ * @param {String} name The name of the shell used in some texts
81
+ * @returns {ShellBuilder}
82
+ */
83
+ withName(name) {
84
+ this.name = name;
85
+ return this;
86
+ }
87
+
88
+ /**
89
+ * Change the prompt displayed when entering text
90
+ * @param {String} prompt The text to be displayed in the prompt
91
+ * @returns {ShellBuilder}
92
+ */
93
+ withPrompt(prompt) {
94
+ this.prompt = prompt;
95
+ return this;
96
+ }
97
+
98
+ /**
99
+ * Add a command handler to be executed if the inputted command doesn't correspond to a built in handler
100
+ * @param {(shell: Shell, input: String) => boolean} handler The handler to be executed with the inputted text and shell passed as arguments.
101
+ * Return true to signal that the handler was successful and shouldn't continue with another handler.
102
+ *
103
+ * @returns {ShellBuilder}
104
+ */
105
+ withCommandHandler(handler) {
106
+ this.commandHandlers.push(handler);
107
+ return this;
108
+ }
109
+
110
+ /**
111
+ * Creates a usable shell with the built-in and custom functions.
112
+ * @returns {Shell}
113
+ */
114
+ build() {
115
+ let shell = new Shell();
116
+ shell.commands = this.commands;
117
+ shell.name = this.name;
118
+ shell.prompt = this.prompt;
119
+ shell.commandHandlers = this.commandHandlers;
120
+ shell.autocomplete = shell.commands.map(c => c.name);
121
+
122
+ return shell;
123
+ }
124
+
125
+ /**
126
+ * Splits a command into args with support for spaces and and escape sequences
127
+ * @private
128
+ * @param {String} input The unformatted command
129
+ * @returns {String[]}
130
+ */
131
+ parseArgs(input) {
132
+ const result = [];
133
+ let current = "";
134
+ let inQuotes = false;
135
+ let escaping = false;
136
+
137
+ for (let i = 0; i < input.length; i++) {
138
+ const char = input[i];
139
+
140
+ if (escaping) {
141
+ current += char;
142
+ escaping = false;
143
+ continue;
144
+ }
145
+
146
+ if (char === "\\") {
147
+ escaping = true;
148
+ continue;
149
+ }
150
+
151
+ if (char === '"') {
152
+ inQuotes = !inQuotes;
153
+ continue;
154
+ }
155
+
156
+ if (char === " " && !inQuotes) {
157
+ if (current.length > 0) {
158
+ result.push(current);
159
+ current = "";
160
+ }
161
+ continue;
162
+ }
163
+
164
+ current += char;
165
+ }
166
+
167
+ if (escaping) {
168
+ current += "\\";
169
+ }
170
+
171
+ if (current.length > 0) {
172
+ result.push(current);
173
+ }
174
+
175
+ return result;
176
+ }
177
+ }