@cosider.construction/eapp 1.0.2 → 1.0.4
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 +1 -1
- package/src/generators/entity.js +28 -11
- package/src/generators/menu.js +24 -21
- package/src/generators/module.js +8 -7
- package/src/generators/sql.js +118 -0
- package/src/generators/view.js +11 -3
- package/src/index.js +65 -19
- package/src/tui/engine.js +141 -105
- package/src/utils/naming.js +40 -16
package/package.json
CHANGED
package/src/generators/entity.js
CHANGED
|
@@ -28,8 +28,8 @@ const AUDIT_WIDTHS = { field: 20, type: 10 };
|
|
|
28
28
|
|
|
29
29
|
// ─── Code generators ──────────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
|
-
function buildModelJs({ module, entity, schema, fields, auditFields, pkField }) {
|
|
32
|
-
const TABLE = toUpper(entity);
|
|
31
|
+
function buildModelJs({ module, entity, table, schema, fields, auditFields, pkField }) {
|
|
32
|
+
const TABLE = toUpper(table || entity);
|
|
33
33
|
const dbAlias = schema === 'dbo' ? 'dbo' : schema;
|
|
34
34
|
const importPath = module === 'shared' ? '../../../engine/db.js' : '../../engine/db.js';
|
|
35
35
|
|
|
@@ -85,7 +85,7 @@ export async function remove(${pkField}) { return model.remove(${pkField}); }
|
|
|
85
85
|
`;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function buildIpcJson({ module, entity, schema }) {
|
|
88
|
+
function buildIpcJson({ module, entity, table, schema }) {
|
|
89
89
|
const route = ipcRoute(module, entity);
|
|
90
90
|
const mod = module === 'shared' ? 'shared' : module;
|
|
91
91
|
const ent = toLower(entity);
|
|
@@ -121,9 +121,17 @@ function buildMetaJson({ module, entity, schema, fields, auditFields, pkField })
|
|
|
121
121
|
|
|
122
122
|
// ─── Write entity files ───────────────────────────────────────────────────────
|
|
123
123
|
|
|
124
|
-
async function writeEntity({ module, entity, schema, fields, auditFields, pkField, opts, dryRun }) {
|
|
124
|
+
async function writeEntity({ module, entity, table, schema, fields, auditFields, pkField, opts, dryRun }) {
|
|
125
125
|
const cwd = process.cwd();
|
|
126
|
-
|
|
126
|
+
// If user typed POLE.dbo it means entity=pole, schema=dbo — show confirmation
|
|
127
|
+
const displayPath = `${module}/${entity}` + (parsedTable !== entity ? `:${parsedTable}` : '') + `.${schema}`;
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(chalk.bold.white(' Entity ') + chalk.cyan(displayPath));
|
|
130
|
+
if (parsedTable !== entity) {
|
|
131
|
+
console.log(chalk.dim(` table name in DB will be: ${toUpper(parsedTable)}`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const backendBase = module === 'shared'
|
|
127
135
|
? path.join(cwd, 'src', 'backend', 'shared', toUpper(entity))
|
|
128
136
|
: path.join(cwd, 'src', 'backend', 'modules', toUpper(module), toUpper(entity));
|
|
129
137
|
|
|
@@ -131,7 +139,7 @@ async function writeEntity({ module, entity, schema, fields, auditFields, pkFiel
|
|
|
131
139
|
|
|
132
140
|
if (o.model) {
|
|
133
141
|
await writeFile(path.join(backendBase, 'model.js'),
|
|
134
|
-
buildModelJs({ module, entity, schema, fields, auditFields, pkField }), { dryRun });
|
|
142
|
+
buildModelJs({ module, entity, table: table || entity, schema, fields, auditFields, pkField }), { dryRun });
|
|
135
143
|
}
|
|
136
144
|
if (o.controller) {
|
|
137
145
|
await writeFile(path.join(backendBase, 'controller.js'),
|
|
@@ -139,7 +147,7 @@ async function writeEntity({ module, entity, schema, fields, auditFields, pkFiel
|
|
|
139
147
|
}
|
|
140
148
|
if (o.ipc) {
|
|
141
149
|
await writeFile(path.join(backendBase, 'ipc.json'),
|
|
142
|
-
buildIpcJson({ module, entity, schema }), { dryRun });
|
|
150
|
+
buildIpcJson({ module, entity, table: table || entity, schema }), { dryRun });
|
|
143
151
|
}
|
|
144
152
|
// Always write meta so registry can read it
|
|
145
153
|
await writeJson(path.join(backendBase, 'entity.meta.json'),
|
|
@@ -186,12 +194,21 @@ export async function generateEntity(args, rawArgs) {
|
|
|
186
194
|
catch (e) { log.error(e.message); process.exit(1); }
|
|
187
195
|
}
|
|
188
196
|
|
|
189
|
-
const { module, entity, schema } = parsed;
|
|
197
|
+
const { module, entity, table: parsedTable, schema } = parsed;
|
|
190
198
|
const pkField = `${entity}_id`;
|
|
199
|
+
let tableOverride = parsedTable !== entity ? parsedTable : null;
|
|
191
200
|
|
|
192
201
|
// ── Check if exists ───────────────────────────────────────────────────────
|
|
193
202
|
const cwd = process.cwd();
|
|
194
|
-
|
|
203
|
+
// If user typed POLE.dbo it means entity=pole, schema=dbo — show confirmation
|
|
204
|
+
const displayPath = `${module}/${entity}` + (parsedTable !== entity ? `:${parsedTable}` : '') + `.${schema}`;
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(chalk.bold.white(' Entity ') + chalk.cyan(displayPath));
|
|
207
|
+
if (parsedTable !== entity) {
|
|
208
|
+
console.log(chalk.dim(` table name in DB will be: ${toUpper(parsedTable)}`));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const backendBase = module === 'shared'
|
|
195
212
|
? path.join(cwd, 'src', 'backend', 'shared', toUpper(entity))
|
|
196
213
|
: path.join(cwd, 'src', 'backend', 'modules', toUpper(module), toUpper(entity));
|
|
197
214
|
|
|
@@ -204,7 +221,7 @@ export async function generateEntity(args, rawArgs) {
|
|
|
204
221
|
// ── --default: skip all prompts ───────────────────────────────────────────
|
|
205
222
|
if (useDefault) {
|
|
206
223
|
await writeEntity({
|
|
207
|
-
module, entity, schema,
|
|
224
|
+
module, entity, table: parsedTable || entity, schema,
|
|
208
225
|
fields: defaultFillableRows(entity),
|
|
209
226
|
auditFields: DEFAULT_AUDIT_ROWS,
|
|
210
227
|
pkField,
|
|
@@ -246,7 +263,7 @@ export async function generateEntity(args, rawArgs) {
|
|
|
246
263
|
const resolvedPk = pkRow ? pkRow.field : pkField;
|
|
247
264
|
|
|
248
265
|
await writeEntity({
|
|
249
|
-
module, entity, schema,
|
|
266
|
+
module, entity, table: tableOverride || entity, schema,
|
|
250
267
|
fields: fillableRows,
|
|
251
268
|
auditFields: auditRows,
|
|
252
269
|
pkField: resolvedPk,
|
package/src/generators/menu.js
CHANGED
|
@@ -12,11 +12,12 @@ function flattenItems(items = [], parentLabel = '') {
|
|
|
12
12
|
const rows = [];
|
|
13
13
|
for (const item of items) {
|
|
14
14
|
rows.push({
|
|
15
|
-
label: item.label
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
label: item.label || '',
|
|
16
|
+
icon: item.icon || '',
|
|
17
|
+
path: parentLabel,
|
|
18
|
+
route: item.route || '',
|
|
19
|
+
dot: item.dot ? 'on' : 'off',
|
|
20
|
+
nbr: item.nbr || '',
|
|
20
21
|
});
|
|
21
22
|
if (item.children?.length) {
|
|
22
23
|
rows.push(...flattenItems(item.children, item.label));
|
|
@@ -28,17 +29,19 @@ function flattenItems(items = [], parentLabel = '') {
|
|
|
28
29
|
// ─── Rebuild nested menu items from flat rows ─────────────────────────────────
|
|
29
30
|
|
|
30
31
|
function nestItems(rows) {
|
|
31
|
-
const top = rows.filter(r => !r.
|
|
32
|
-
const children = rows.filter(r =>
|
|
32
|
+
const top = rows.filter(r => !r.path);
|
|
33
|
+
const children = rows.filter(r => r.path);
|
|
33
34
|
|
|
34
35
|
return top.map(r => {
|
|
35
|
-
const item = { label: r.label,
|
|
36
|
-
if (r.
|
|
37
|
-
|
|
36
|
+
const item = { label: r.label, icon: r.icon, route: r.route };
|
|
37
|
+
if (r.dot === 'on') item.dot = true;
|
|
38
|
+
if (r.nbr) item.nbr = r.nbr;
|
|
39
|
+
const kids = children.filter(c => c.path === r.label);
|
|
38
40
|
if (kids.length) {
|
|
39
41
|
item.children = kids.map(c => ({
|
|
40
|
-
label: c.label,
|
|
41
|
-
...(c.
|
|
42
|
+
label: c.label, icon: c.icon, route: c.route,
|
|
43
|
+
...(c.dot === 'on' ? { dot: true } : {}),
|
|
44
|
+
...(c.nbr ? { nbr: c.nbr } : {}),
|
|
42
45
|
}));
|
|
43
46
|
}
|
|
44
47
|
return item;
|
|
@@ -59,7 +62,7 @@ async function editViewMenu(menuFile, existing) {
|
|
|
59
62
|
// Classic — same item table as module menu
|
|
60
63
|
const existing_items = flattenItems(existing?.hMenu?.items || []);
|
|
61
64
|
if (!existing_items.length) {
|
|
62
|
-
existing_items.push({ label: 'Action',
|
|
65
|
+
existing_items.push({ label: 'Action', icon: '', path: '', route: '', dot: 'off', nbr: '' });
|
|
63
66
|
}
|
|
64
67
|
console.log('');
|
|
65
68
|
const rows = await fieldsTable(
|
|
@@ -185,14 +188,14 @@ export async function generateMenu(args, rawArgs) {
|
|
|
185
188
|
console.log(chalk.bold.white(` ${toPascal(module)} — hMenu`));
|
|
186
189
|
const hRows = flattenItems(existing.hMenu?.items || []);
|
|
187
190
|
if (!hRows.length) {
|
|
188
|
-
hRows.push({ label: `${toPascal(module)} Home`, route: `/${module}`,
|
|
191
|
+
hRows.push({ label: `${toPascal(module)} Home`, icon: '', path: '', route: `/${module}`, dot: 'off', nbr: '' });
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
const updatedH = await fieldsTable(
|
|
192
195
|
hRows,
|
|
193
|
-
['label', '
|
|
194
|
-
{ label: 16,
|
|
195
|
-
'hMenu (
|
|
196
|
+
['label', 'icon', 'path', 'route', 'dot', 'nbr'],
|
|
197
|
+
{ label: 16, icon: 5, path: 14, route: 20, dot: 5, nbr: 6 },
|
|
198
|
+
'hMenu (path = parent group label, dot = show dot indicator, nbr = badge)'
|
|
196
199
|
);
|
|
197
200
|
|
|
198
201
|
// Pass 2 — vMenu
|
|
@@ -200,14 +203,14 @@ export async function generateMenu(args, rawArgs) {
|
|
|
200
203
|
console.log(chalk.bold.white(` ${toPascal(module)} — vMenu`));
|
|
201
204
|
const vRows = flattenItems(existing.vMenu?.items || []);
|
|
202
205
|
if (!vRows.length) {
|
|
203
|
-
vRows.push({ label: toPascal(module), route: `/${module}`,
|
|
206
|
+
vRows.push({ label: toPascal(module), icon: '', path: '', route: `/${module}`, dot: 'off', nbr: '' });
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
const updatedV = await fieldsTable(
|
|
207
210
|
vRows,
|
|
208
|
-
['label', '
|
|
209
|
-
{ label: 16,
|
|
210
|
-
'vMenu (
|
|
211
|
+
['label', 'icon', 'path', 'route', 'dot', 'nbr'],
|
|
212
|
+
{ label: 16, icon: 5, path: 14, route: 20, dot: 5, nbr: 6 },
|
|
213
|
+
'vMenu (path = collapsible group label, dot = show dot indicator, nbr = badge)'
|
|
211
214
|
);
|
|
212
215
|
|
|
213
216
|
// Also check hMenu style
|
package/src/generators/module.js
CHANGED
|
@@ -121,7 +121,7 @@ export async function generateModule(args, rawArgs) {
|
|
|
121
121
|
return {
|
|
122
122
|
module: lower,
|
|
123
123
|
layout: `${toPascal(lower)}Layout`,
|
|
124
|
-
|
|
124
|
+
hmenu: 'own',
|
|
125
125
|
home: 'on',
|
|
126
126
|
grid: 'on',
|
|
127
127
|
};
|
|
@@ -138,7 +138,7 @@ export async function generateModule(args, rawArgs) {
|
|
|
138
138
|
newName = toLower(newName.trim());
|
|
139
139
|
if (!isValidName(newName)) { log.error('Invalid module name.'); process.exit(1); }
|
|
140
140
|
if (!existing.find(e => toLower(e) === newName)) {
|
|
141
|
-
rows.push({ module: newName, layout: `${toPascal(newName)}Layout`,
|
|
141
|
+
rows.push({ module: newName, layout: `${toPascal(newName)}Layout`, hmenu: 'own', home: 'on', grid: '3x2' });
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -146,17 +146,18 @@ export async function generateModule(args, rawArgs) {
|
|
|
146
146
|
console.log('');
|
|
147
147
|
const updated = await fieldsTable(
|
|
148
148
|
rows,
|
|
149
|
-
['module', 'layout', '
|
|
150
|
-
{ module: 14, layout: 18,
|
|
151
|
-
'Modules (
|
|
149
|
+
['module', 'layout', 'hmenu', 'home', 'grid'],
|
|
150
|
+
{ module: 14, layout: 18, hmenu: 8, home: 6, grid: 8 },
|
|
151
|
+
'Modules (hmenu: own|share|none home: on|off grid: 0 or NxM e.g. 3x2 enter to save)'
|
|
152
152
|
);
|
|
153
153
|
|
|
154
154
|
// ── Write new/updated modules ──────────────────────────────────────────────
|
|
155
155
|
for (const row of updated) {
|
|
156
156
|
const name = toLower(row.module);
|
|
157
157
|
const hasHome = row.home !== 'off';
|
|
158
|
-
const
|
|
159
|
-
const
|
|
158
|
+
const gridVal = row.grid || '0'; // '0' = no grid, 'NxM' = N cols x M rows
|
|
159
|
+
const hasGrid = gridVal !== '0' && gridVal !== 'off';
|
|
160
|
+
const hMenuScope = row.hmenu || 'own'; // own | share | none
|
|
160
161
|
const layoutName = row.layout || `${toPascal(name)}Layout`;
|
|
161
162
|
|
|
162
163
|
const backendDir = path.join(cwd, 'src', 'backend', 'modules', toUpper(name));
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { toUpper, toLower, toPascal } from '../utils/naming.js';
|
|
5
|
+
import { writeFile, assertProjectRoot, loadEntityRegistry } from '../utils/fs.js';
|
|
6
|
+
import { log } from '../utils/log.js';
|
|
7
|
+
import { radioLine, textInput } from '../tui/engine.js';
|
|
8
|
+
|
|
9
|
+
// ─── SQL generators ───────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function sqlTypeMap(appType) {
|
|
12
|
+
const map = {
|
|
13
|
+
varchar: 'NVARCHAR(255)',
|
|
14
|
+
int: 'INT',
|
|
15
|
+
decimal: 'DECIMAL(18,2)',
|
|
16
|
+
date: 'DATE',
|
|
17
|
+
datetime: 'DATETIME2',
|
|
18
|
+
bit: 'BIT',
|
|
19
|
+
text: 'NVARCHAR(MAX)',
|
|
20
|
+
float: 'FLOAT',
|
|
21
|
+
enum: 'NVARCHAR(50)',
|
|
22
|
+
};
|
|
23
|
+
return map[appType] || 'NVARCHAR(255)';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildCreateTable({ entity, table, schema, fields, auditFields, pkField }) {
|
|
27
|
+
const TABLE = toUpper(table || entity);
|
|
28
|
+
const SCHEMA = schema || 'dbo';
|
|
29
|
+
const allCols = [...(fields || []), ...(auditFields || [])];
|
|
30
|
+
|
|
31
|
+
const colLines = allCols.map(f => {
|
|
32
|
+
const nullable = f.pk ? 'NOT NULL' : 'NULL';
|
|
33
|
+
const identity = f.pk ? ' IDENTITY(1,1)' : '';
|
|
34
|
+
return ` [${f.field}] ${sqlTypeMap(f.type)}${identity} ${nullable}`;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const pkLine = ` CONSTRAINT [PK_${TABLE}] PRIMARY KEY ([${pkField || `${entity}_id`}])`;
|
|
38
|
+
|
|
39
|
+
return `-- Generated by eapp — ${new Date().toISOString().slice(0, 10)}
|
|
40
|
+
-- Entity : ${toLower(entity)}
|
|
41
|
+
-- Table : [${SCHEMA}].[${TABLE}]
|
|
42
|
+
|
|
43
|
+
CREATE TABLE [${SCHEMA}].[${TABLE}] (
|
|
44
|
+
${[...colLines, pkLine].join(',\n')}
|
|
45
|
+
);
|
|
46
|
+
GO
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildAlterTable({ entity, table, schema, fields }) {
|
|
51
|
+
const TABLE = toUpper(table || entity);
|
|
52
|
+
const SCHEMA = schema || 'dbo';
|
|
53
|
+
|
|
54
|
+
const lines = (fields || []).map(f =>
|
|
55
|
+
`ALTER TABLE [${SCHEMA}].[${TABLE}] ADD [${f.field}] ${sqlTypeMap(f.type)} NULL;`
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return `-- Alter: add missing columns to [${SCHEMA}].[${TABLE}]
|
|
59
|
+
${lines.join('\n')}
|
|
60
|
+
GO
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Main generator ───────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export async function generateSql(args, rawArgs) {
|
|
67
|
+
assertProjectRoot();
|
|
68
|
+
|
|
69
|
+
const cwd = process.cwd();
|
|
70
|
+
const registry = await loadEntityRegistry(cwd);
|
|
71
|
+
|
|
72
|
+
if (!registry.size) {
|
|
73
|
+
log.warn('No entities found. Run eapp entity first.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Pick entity from registry
|
|
78
|
+
const entries = [...registry.entries()];
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(chalk.bold.white(' Generate SQL for entity'));
|
|
81
|
+
const key = await textInput('module/entity (or "all"):');
|
|
82
|
+
|
|
83
|
+
const targets = key.trim() === 'all'
|
|
84
|
+
? entries
|
|
85
|
+
: entries.filter(([k]) => k.endsWith(key.trim().toLowerCase()) || k === key.trim().toLowerCase());
|
|
86
|
+
|
|
87
|
+
if (!targets.length) {
|
|
88
|
+
log.error(`Entity "${key}" not found in registry.`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Pick statement type
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(chalk.bold.white(' Statement type'));
|
|
95
|
+
const stmtType = await radioLine('', [
|
|
96
|
+
{ label: 'CREATE TABLE', value: 'create' },
|
|
97
|
+
{ label: 'ALTER (add cols)', value: 'alter' },
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const sqlDir = path.join(cwd, 'src', 'backend', 'sql');
|
|
101
|
+
await fs.ensureDir(sqlDir);
|
|
102
|
+
|
|
103
|
+
for (const [, meta] of targets) {
|
|
104
|
+
const { module, entity, table, schema, fields, auditFields, pkField } = meta;
|
|
105
|
+
const fileName = stmtType === 'create'
|
|
106
|
+
? `${schema}_${toUpper(table || entity)}_create.sql`
|
|
107
|
+
: `${schema}_${toUpper(table || entity)}_alter.sql`;
|
|
108
|
+
const filePath = path.join(sqlDir, fileName);
|
|
109
|
+
const content = stmtType === 'create'
|
|
110
|
+
? buildCreateTable({ entity, table, schema, fields, auditFields, pkField })
|
|
111
|
+
: buildAlterTable({ entity, table, schema, fields });
|
|
112
|
+
|
|
113
|
+
await writeFile(filePath, content);
|
|
114
|
+
log.success(`${fileName}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
log.info(`SQL files written to src/backend/sql/`);
|
|
118
|
+
}
|
package/src/generators/view.js
CHANGED
|
@@ -349,11 +349,19 @@ export async function generateView(args, rawArgs) {
|
|
|
349
349
|
defaultComps,
|
|
350
350
|
COMP_COLS,
|
|
351
351
|
COMP_WIDTHS,
|
|
352
|
-
'Components (+ add - remove
|
|
352
|
+
'Components (+ add - remove \u2190 \u2192 cols space menu toggle enter done)'
|
|
353
353
|
);
|
|
354
354
|
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
// List render mode — explicit radio (table / card / both) so user can't mistype
|
|
356
|
+
console.log('');
|
|
357
|
+
console.log(chalk.bold.white(' List render'));
|
|
358
|
+
const listRender = await radioLine('', [
|
|
359
|
+
{ label: 'Table (row)', value: 'table' },
|
|
360
|
+
{ label: 'Card', value: 'card' },
|
|
361
|
+
{ label: 'Both', value: 'both' },
|
|
362
|
+
]);
|
|
363
|
+
const hasRow = listRender === 'table' || listRender === 'both';
|
|
364
|
+
const hasCard = listRender === 'card' || listRender === 'both';
|
|
357
365
|
|
|
358
366
|
// ── Step 4: slaves ────────────────────────────────────────────────────────
|
|
359
367
|
let slaveConfigs = [];
|
package/src/index.js
CHANGED
|
@@ -39,28 +39,74 @@ Options:
|
|
|
39
39
|
--help, -h Show this help
|
|
40
40
|
`;
|
|
41
41
|
|
|
42
|
+
const MAIN_MENU = [
|
|
43
|
+
{ label: 'Entity', value: 'entity' },
|
|
44
|
+
{ label: 'Pivot', value: 'pivot' },
|
|
45
|
+
{ label: 'View', value: 'view' },
|
|
46
|
+
{ label: 'Module', value: 'module' },
|
|
47
|
+
{ label: 'Menu', value: 'menu' },
|
|
48
|
+
{ label: 'Sql', value: 'sql' },
|
|
49
|
+
{ label: 'Config', value: 'config' },
|
|
50
|
+
{ label: 'Layout', value: 'layout' },
|
|
51
|
+
{ label: 'List', value: 'list' },
|
|
52
|
+
{ label: 'Quit', value: 'quit' },
|
|
53
|
+
];
|
|
54
|
+
|
|
42
55
|
async function launchInteractive() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log('');
|
|
57
|
-
if (chosen === 'list') {
|
|
58
|
-
await listModules();
|
|
56
|
+
let lastIdx = 0;
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line no-constant-condition
|
|
59
|
+
while (true) {
|
|
60
|
+
// Clear screen so each iteration feels fresh
|
|
61
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(chalk.bold.cyan(' eapp') + chalk.dim(' — what do you want to do?'));
|
|
64
|
+
console.log(chalk.dim(' ← → select enter confirm Quit to exit'));
|
|
65
|
+
|
|
66
|
+
const chosen = await radioLine('', MAIN_MENU, lastIdx);
|
|
67
|
+
lastIdx = MAIN_MENU.findIndex(m => m.value === chosen);
|
|
68
|
+
|
|
59
69
|
console.log('');
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
|
|
71
|
+
if (chosen === 'quit') {
|
|
72
|
+
console.log(chalk.dim(' Bye.\n'));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (chosen === 'sql') {
|
|
77
|
+
const { generateSql } = await import('./generators/sql.js');
|
|
78
|
+
await generateSql([], []);
|
|
79
|
+
} else if (chosen === 'list') {
|
|
80
|
+
await listModules();
|
|
81
|
+
console.log('');
|
|
82
|
+
await listEntities([]);
|
|
83
|
+
} else {
|
|
84
|
+
await run(chosen, [], []);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Pause briefly so the user can read the result before the menu redraws
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(chalk.dim(' Press enter to return to the menu…'));
|
|
90
|
+
await waitEnter();
|
|
62
91
|
}
|
|
63
|
-
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function waitEnter() {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
process.stdin.resume();
|
|
97
|
+
process.stdin.setEncoding('utf8');
|
|
98
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
99
|
+
const onKey = (key) => {
|
|
100
|
+
if (key === '\r' || key === '\n' || key === '\x03') {
|
|
101
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
102
|
+
process.stdin.pause();
|
|
103
|
+
process.stdin.removeListener('data', onKey);
|
|
104
|
+
if (key === '\x03') process.exit(0);
|
|
105
|
+
resolve();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
process.stdin.on('data', onKey);
|
|
109
|
+
});
|
|
64
110
|
}
|
|
65
111
|
|
|
66
112
|
async function run(command, args, rawArgs) {
|
package/src/tui/engine.js
CHANGED
|
@@ -10,42 +10,37 @@ import chalk from 'chalk';
|
|
|
10
10
|
// ─── Raw input helpers ────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
export function enableRawInput() {
|
|
13
|
-
if (process.stdin.isTTY)
|
|
14
|
-
process.stdin.setRawMode(true);
|
|
15
|
-
}
|
|
13
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
16
14
|
process.stdin.resume();
|
|
17
15
|
process.stdin.setEncoding('utf8');
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export function disableRawInput() {
|
|
21
|
-
if (process.stdin.isTTY)
|
|
22
|
-
process.stdin.setRawMode(false);
|
|
23
|
-
}
|
|
19
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
24
20
|
process.stdin.pause();
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
export function clearLines(n) {
|
|
28
|
-
for (let i = 0; i < n; i++)
|
|
29
|
-
process.stdout.write('\x1B[1A\x1B[2K');
|
|
30
|
-
}
|
|
24
|
+
for (let i = 0; i < n; i++) process.stdout.write('\x1B[1A\x1B[2K');
|
|
31
25
|
}
|
|
32
26
|
|
|
33
|
-
export function
|
|
34
|
-
export function
|
|
27
|
+
export function clearScreen() { process.stdout.write('\x1B[2J\x1B[H'); }
|
|
28
|
+
export function hideCursor() { process.stdout.write('\x1B[?25l'); }
|
|
29
|
+
export function showCursor() { process.stdout.write('\x1B[?25h'); }
|
|
35
30
|
|
|
36
31
|
// ─── Key constants ────────────────────────────────────────────────────────────
|
|
37
32
|
|
|
38
33
|
export const KEYS = {
|
|
39
|
-
UP:
|
|
40
|
-
DOWN:
|
|
41
|
-
RIGHT:
|
|
42
|
-
LEFT:
|
|
43
|
-
ENTER:
|
|
44
|
-
SPACE:
|
|
45
|
-
PLUS:
|
|
46
|
-
MINUS:
|
|
47
|
-
ESC:
|
|
48
|
-
CTRL_C:
|
|
34
|
+
UP: '\x1B[A',
|
|
35
|
+
DOWN: '\x1B[B',
|
|
36
|
+
RIGHT: '\x1B[C',
|
|
37
|
+
LEFT: '\x1B[D',
|
|
38
|
+
ENTER: '\r',
|
|
39
|
+
SPACE: ' ',
|
|
40
|
+
PLUS: '+',
|
|
41
|
+
MINUS: '-',
|
|
42
|
+
ESC: '\x1B',
|
|
43
|
+
CTRL_C: '\x03',
|
|
49
44
|
BACKSPACE: '\x7F',
|
|
50
45
|
};
|
|
51
46
|
|
|
@@ -65,8 +60,8 @@ export async function textInput(prompt, defaultVal = '') {
|
|
|
65
60
|
// ─── Radio selector ───────────────────────────────────────────────────────────
|
|
66
61
|
|
|
67
62
|
/**
|
|
68
|
-
* Horizontal radio — one choice from options
|
|
69
|
-
* Returns selected value
|
|
63
|
+
* Horizontal radio — one choice from options.
|
|
64
|
+
* Returns selected value.
|
|
70
65
|
*/
|
|
71
66
|
export async function radioLine(prompt, options, defaultIdx = 0) {
|
|
72
67
|
let selected = defaultIdx;
|
|
@@ -75,9 +70,7 @@ export async function radioLine(prompt, options, defaultIdx = 0) {
|
|
|
75
70
|
process.stdout.clearLine?.(0);
|
|
76
71
|
process.stdout.cursorTo?.(0);
|
|
77
72
|
const opts = options.map((o, i) =>
|
|
78
|
-
i === selected
|
|
79
|
-
? chalk.green(`◉ ${o.label}`)
|
|
80
|
-
: chalk.dim(`○ ${o.label}`)
|
|
73
|
+
i === selected ? chalk.green(`◉ ${o.label}`) : chalk.dim(`○ ${o.label}`)
|
|
81
74
|
).join(' ');
|
|
82
75
|
process.stdout.write(` ${chalk.cyan(prompt)} ${opts}`);
|
|
83
76
|
};
|
|
@@ -90,9 +83,9 @@ export async function radioLine(prompt, options, defaultIdx = 0) {
|
|
|
90
83
|
|
|
91
84
|
const onKey = (key) => {
|
|
92
85
|
if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
|
|
93
|
-
if (key === KEYS.LEFT)
|
|
94
|
-
if (key === KEYS.RIGHT)
|
|
95
|
-
if (key === KEYS.ENTER)
|
|
86
|
+
if (key === KEYS.LEFT) { selected = (selected - 1 + options.length) % options.length; render(); }
|
|
87
|
+
if (key === KEYS.RIGHT) { selected = (selected + 1) % options.length; render(); }
|
|
88
|
+
if (key === KEYS.ENTER) {
|
|
96
89
|
process.stdout.write('\n');
|
|
97
90
|
process.stdin.removeListener('data', onKey);
|
|
98
91
|
showCursor();
|
|
@@ -107,43 +100,46 @@ export async function radioLine(prompt, options, defaultIdx = 0) {
|
|
|
107
100
|
// ─── Options toggles ──────────────────────────────────────────────────────────
|
|
108
101
|
|
|
109
102
|
/**
|
|
110
|
-
* One-line toggleable options
|
|
103
|
+
* One-line toggleable options.
|
|
111
104
|
* Returns object { [key]: boolean }
|
|
112
105
|
*/
|
|
113
106
|
export async function optionsLine(options) {
|
|
114
|
-
// options = [{ key, label, default }]
|
|
115
107
|
const state = {};
|
|
116
108
|
options.forEach(o => { state[o.key] = o.default !== false; });
|
|
117
109
|
let cursor = 0;
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
111
|
+
// Lines: 1 (options row) + 1 (hint row) — always exactly 2
|
|
112
|
+
const RENDERED_LINES = 2;
|
|
113
|
+
|
|
114
|
+
const renderRow = () => {
|
|
115
|
+
return options.map((o, i) => {
|
|
116
|
+
const on = state[o.key];
|
|
117
|
+
const box = on ? chalk.green('◼') : chalk.dim('◻');
|
|
118
|
+
const label = i === cursor
|
|
119
|
+
? chalk.bold.white(o.label)
|
|
120
|
+
: (on ? chalk.white(o.label) : chalk.dim(o.label));
|
|
125
121
|
return `${box} ${label}`;
|
|
126
122
|
}).join(' ');
|
|
127
|
-
console.log(' ' + rendered);
|
|
128
|
-
console.log(chalk.dim(' ← → navigate space toggle enter confirm'));
|
|
129
123
|
};
|
|
130
124
|
|
|
131
125
|
return new Promise((resolve) => {
|
|
132
126
|
hideCursor();
|
|
133
127
|
enableRawInput();
|
|
134
128
|
console.log('');
|
|
135
|
-
|
|
136
|
-
const on = state[o.key];
|
|
137
|
-
return `${on ? chalk.green('◼') : chalk.dim('◻')} ${on ? chalk.white(o.label) : chalk.dim(o.label)}`;
|
|
138
|
-
}).join(' ');
|
|
139
|
-
console.log(' ' + rendered);
|
|
129
|
+
console.log(' ' + renderRow());
|
|
140
130
|
console.log(chalk.dim(' ← → navigate space toggle enter confirm'));
|
|
141
131
|
|
|
142
132
|
const onKey = (key) => {
|
|
143
133
|
if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
|
|
144
|
-
if (key === KEYS.LEFT)
|
|
145
|
-
if (key === KEYS.RIGHT)
|
|
146
|
-
if (key === KEYS.SPACE)
|
|
134
|
+
if (key === KEYS.LEFT) { cursor = (cursor - 1 + options.length) % options.length; }
|
|
135
|
+
if (key === KEYS.RIGHT) { cursor = (cursor + 1) % options.length; }
|
|
136
|
+
if (key === KEYS.SPACE) { state[options[cursor].key] = !state[options[cursor].key]; }
|
|
137
|
+
if (key === KEYS.LEFT || key === KEYS.RIGHT || key === KEYS.SPACE) {
|
|
138
|
+
clearLines(RENDERED_LINES);
|
|
139
|
+
console.log(' ' + renderRow());
|
|
140
|
+
console.log(chalk.dim(' ← → navigate space toggle enter confirm'));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
147
143
|
if (key === KEYS.ENTER) {
|
|
148
144
|
process.stdin.removeListener('data', onKey);
|
|
149
145
|
showCursor();
|
|
@@ -157,72 +153,108 @@ export async function optionsLine(options) {
|
|
|
157
153
|
|
|
158
154
|
// ─── Interactive table ────────────────────────────────────────────────────────
|
|
159
155
|
|
|
160
|
-
const COL_WIDTHS = {
|
|
161
|
-
field: 18,
|
|
162
|
-
type: 10,
|
|
163
|
-
values: 14,
|
|
164
|
-
pk: 4,
|
|
165
|
-
fk: 4,
|
|
166
|
-
group: 14,
|
|
167
|
-
};
|
|
168
|
-
|
|
169
156
|
const FIELD_TYPES = ['varchar', 'int', 'decimal', 'date', 'datetime', 'bit', 'text', 'float', 'enum'];
|
|
170
157
|
|
|
171
|
-
|
|
158
|
+
// Columns where space toggles a boolean (rendered as ◼/◻ checkboxes)
|
|
159
|
+
const BOOL_COLS = new Set(['pk', 'fk']);
|
|
160
|
+
|
|
161
|
+
// Columns where space cycles between string values
|
|
162
|
+
const CYCLE_COLS = {
|
|
163
|
+
vmenu: ['own', 'share'],
|
|
164
|
+
hmenu: ['own', 'share', 'none'],
|
|
165
|
+
home: ['on', 'off'],
|
|
166
|
+
// 'grid' is free-text (NxM), not cycled
|
|
167
|
+
};
|
|
172
168
|
|
|
169
|
+
// Strip ANSI escape codes to get the true printable length of a string.
|
|
170
|
+
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
171
|
+
function visibleLen(s) { return String(s).replace(ANSI_RE, '').length; }
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Pad or truncate str to exactly len visible characters.
|
|
175
|
+
* Too-long strings are cut with a trailing ellipsis so column borders stay aligned.
|
|
176
|
+
*/
|
|
173
177
|
function pad(str, len) {
|
|
174
|
-
const s
|
|
175
|
-
|
|
178
|
+
const s = String(str ?? '');
|
|
179
|
+
const vl = visibleLen(s);
|
|
180
|
+
if (vl === len) return s;
|
|
181
|
+
if (vl > len) return s.replace(ANSI_RE, '').slice(0, len - 1) + '…';
|
|
182
|
+
return s + ' '.repeat(len - vl);
|
|
176
183
|
}
|
|
177
184
|
|
|
178
185
|
function renderTableHeader(cols, widths) {
|
|
179
|
-
const header
|
|
186
|
+
const header = cols.map(c => chalk.bold.cyan(pad(c, widths[c] || 12))).join(chalk.dim('│'));
|
|
180
187
|
const divider = cols.map(c => '─'.repeat(widths[c] || 12)).join('┼');
|
|
181
188
|
return ` ┌${'─'.repeat(divider.length)}┐\n │${header}│\n ├${divider}┤`;
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
function renderFieldRow(row, cols, widths, isActive, activeCol) {
|
|
185
192
|
const cells = cols.map((col, ci) => {
|
|
186
|
-
|
|
187
|
-
if (col === 'pk' || col === 'fk') val = val ? chalk.green('◼') : chalk.dim('◻');
|
|
193
|
+
const colW = widths[col] || 12;
|
|
188
194
|
const isThisCell = isActive && ci === activeCol;
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
|
|
196
|
+
let cell;
|
|
197
|
+
if (BOOL_COLS.has(col)) {
|
|
198
|
+
// Boolean: pad the plain glyph first so width is exact, then colorize
|
|
199
|
+
const glyph = pad(row[col] ? '◼' : '◻', colW);
|
|
200
|
+
cell = isThisCell
|
|
201
|
+
? chalk.bgBlue.white(glyph)
|
|
202
|
+
: (row[col] ? chalk.green(glyph) : chalk.dim(glyph));
|
|
203
|
+
} else {
|
|
204
|
+
// Text / cycle: pad plain text, then colorize — ANSI codes added after padding
|
|
205
|
+
const plain = pad(String(row[col] ?? ''), colW);
|
|
206
|
+
cell = isThisCell
|
|
207
|
+
? chalk.bgBlue.white(plain)
|
|
208
|
+
: (isActive ? chalk.white(plain) : chalk.dim(plain));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return cell;
|
|
191
212
|
}).join(chalk.dim('│'));
|
|
192
213
|
return ` │${cells}│`;
|
|
193
214
|
}
|
|
194
215
|
|
|
195
216
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
217
|
+
* Count of lines the table currently occupies (for accurate clearLines).
|
|
218
|
+
* 1 blank line before title
|
|
219
|
+
* 1 title line
|
|
220
|
+
* 3 header block (top border + header row + divider)
|
|
221
|
+
* N data rows
|
|
222
|
+
* 1 bottom border
|
|
223
|
+
* 1 hint line
|
|
224
|
+
* + type menu lines (when open)
|
|
225
|
+
*/
|
|
226
|
+
function tableLineCount(rowCount, typeMenuOpen) {
|
|
227
|
+
return 1 + 1 + 3 + rowCount + 1 + 1 + (typeMenuOpen ? FIELD_TYPES.length + 2 : 0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function cycleValue(col, current) {
|
|
231
|
+
const opts = CYCLE_COLS[col];
|
|
232
|
+
if (!opts) return current;
|
|
233
|
+
return opts[(opts.indexOf(current) + 1) % opts.length];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Interactive fields table.
|
|
238
|
+
* Returns array of field objects.
|
|
198
239
|
*/
|
|
199
240
|
export async function fieldsTable(initialRows, cols, widths, title) {
|
|
200
241
|
const rows = initialRows.map(r => ({ ...r }));
|
|
201
242
|
let rowIdx = 0;
|
|
202
243
|
let colIdx = 0;
|
|
203
|
-
let editBuffer = null; // currently editing text cell
|
|
204
244
|
let typeMenuOpen = false;
|
|
205
|
-
let typeMenuIdx
|
|
206
|
-
|
|
207
|
-
const toggleCols = cols.filter(c => c === 'pk' || c === 'fk');
|
|
208
|
-
const textCols = cols.filter(c => c !== 'pk' && c !== 'fk');
|
|
209
|
-
|
|
210
|
-
function totalLines() {
|
|
211
|
-
return rows.length + 4; // header(3) + footer(1)
|
|
212
|
-
}
|
|
245
|
+
let typeMenuIdx = 0;
|
|
213
246
|
|
|
214
247
|
function render(initial = false) {
|
|
215
|
-
if (!initial) clearLines(
|
|
248
|
+
if (!initial) clearLines(tableLineCount(rows.length, typeMenuOpen));
|
|
216
249
|
|
|
217
250
|
console.log('\n ' + chalk.bold.white(title));
|
|
218
251
|
console.log(renderTableHeader(cols, widths));
|
|
219
|
-
|
|
220
252
|
rows.forEach((row, ri) => {
|
|
221
253
|
console.log(renderFieldRow(row, cols, widths, ri === rowIdx, colIdx));
|
|
222
254
|
});
|
|
223
|
-
|
|
224
|
-
console.log(` └${'─'.repeat(
|
|
225
|
-
console.log(chalk.dim(' ← → cols ↑ ↓ rows + add
|
|
255
|
+
const totalW = cols.map(c => widths[c] || 12).reduce((a, b) => a + b + 1, -1);
|
|
256
|
+
console.log(` └${'─'.repeat(totalW)}┘`);
|
|
257
|
+
console.log(chalk.dim(' ← → cols ↑ ↓ rows + add - del space toggle/cycle enter done'));
|
|
226
258
|
|
|
227
259
|
if (typeMenuOpen) {
|
|
228
260
|
console.log('');
|
|
@@ -240,30 +272,29 @@ export async function fieldsTable(initialRows, cols, widths, title) {
|
|
|
240
272
|
const onKey = (key) => {
|
|
241
273
|
if (key === KEYS.CTRL_C) { showCursor(); disableRawInput(); process.exit(0); }
|
|
242
274
|
|
|
243
|
-
// Type
|
|
275
|
+
// ── Type picker ─────────────────────────────────────────────────────────
|
|
244
276
|
if (typeMenuOpen) {
|
|
245
277
|
if (key === KEYS.UP) { typeMenuIdx = (typeMenuIdx - 1 + FIELD_TYPES.length) % FIELD_TYPES.length; render(); return; }
|
|
246
278
|
if (key === KEYS.DOWN) { typeMenuIdx = (typeMenuIdx + 1) % FIELD_TYPES.length; render(); return; }
|
|
247
|
-
if (key === KEYS.ENTER) {
|
|
248
|
-
|
|
249
|
-
typeMenuOpen = false;
|
|
250
|
-
render();
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (key === KEYS.ESC) { typeMenuOpen = false; render(); return; }
|
|
279
|
+
if (key === KEYS.ENTER) { rows[rowIdx].type = FIELD_TYPES[typeMenuIdx]; typeMenuOpen = false; render(); return; }
|
|
280
|
+
if (key === KEYS.ESC) { typeMenuOpen = false; render(); return; }
|
|
254
281
|
return;
|
|
255
282
|
}
|
|
256
283
|
|
|
257
|
-
// Navigation
|
|
258
|
-
if (key === KEYS.UP) { rowIdx = Math.max(0, rowIdx - 1);
|
|
259
|
-
if (key === KEYS.DOWN) { rowIdx = Math.min(rows.length - 1, rowIdx + 1);
|
|
260
|
-
if (key === KEYS.LEFT) { colIdx = Math.max(0, colIdx - 1);
|
|
261
|
-
if (key === KEYS.RIGHT) { colIdx = Math.min(cols.length - 1, colIdx + 1);
|
|
284
|
+
// ── Navigation ──────────────────────────────────────────────────────────
|
|
285
|
+
if (key === KEYS.UP) { rowIdx = Math.max(0, rowIdx - 1); render(); return; }
|
|
286
|
+
if (key === KEYS.DOWN) { rowIdx = Math.min(rows.length - 1, rowIdx + 1); render(); return; }
|
|
287
|
+
if (key === KEYS.LEFT) { colIdx = Math.max(0, colIdx - 1); render(); return; }
|
|
288
|
+
if (key === KEYS.RIGHT) { colIdx = Math.min(cols.length - 1, colIdx + 1); render(); return; }
|
|
262
289
|
|
|
263
|
-
// Add / remove rows
|
|
290
|
+
// ── Add / remove rows ───────────────────────────────────────────────────
|
|
264
291
|
if (key === KEYS.PLUS) {
|
|
265
292
|
const newRow = {};
|
|
266
|
-
cols.forEach(c => {
|
|
293
|
+
cols.forEach(c => {
|
|
294
|
+
if (BOOL_COLS.has(c)) newRow[c] = false;
|
|
295
|
+
else if (CYCLE_COLS[c]) newRow[c] = CYCLE_COLS[c][0];
|
|
296
|
+
else newRow[c] = '';
|
|
297
|
+
});
|
|
267
298
|
rows.splice(rowIdx + 1, 0, newRow);
|
|
268
299
|
rowIdx++;
|
|
269
300
|
render();
|
|
@@ -275,29 +306,35 @@ export async function fieldsTable(initialRows, cols, widths, title) {
|
|
|
275
306
|
return;
|
|
276
307
|
}
|
|
277
308
|
|
|
278
|
-
//
|
|
309
|
+
// ── Space ───────────────────────────────────────────────────────────────
|
|
279
310
|
if (key === KEYS.SPACE) {
|
|
280
311
|
const col = cols[colIdx];
|
|
281
|
-
|
|
312
|
+
|
|
313
|
+
if (BOOL_COLS.has(col)) {
|
|
282
314
|
rows[rowIdx][col] = !rows[rowIdx][col];
|
|
283
|
-
//
|
|
315
|
+
// Enforce single PK
|
|
284
316
|
if (col === 'pk' && rows[rowIdx][col]) {
|
|
285
317
|
rows.forEach((r, i) => { if (i !== rowIdx) r.pk = false; });
|
|
286
318
|
}
|
|
287
319
|
render();
|
|
288
320
|
return;
|
|
289
321
|
}
|
|
290
|
-
|
|
322
|
+
|
|
323
|
+
if (CYCLE_COLS[col]) {
|
|
324
|
+
rows[rowIdx][col] = cycleValue(col, rows[rowIdx][col]);
|
|
325
|
+
render();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
291
329
|
if (col === 'type') {
|
|
292
330
|
typeMenuOpen = true;
|
|
293
|
-
typeMenuIdx
|
|
294
|
-
if (typeMenuIdx < 0) typeMenuIdx = 0;
|
|
331
|
+
typeMenuIdx = Math.max(0, FIELD_TYPES.indexOf(rows[rowIdx].type || 'varchar'));
|
|
295
332
|
render();
|
|
296
333
|
return;
|
|
297
334
|
}
|
|
298
335
|
}
|
|
299
336
|
|
|
300
|
-
//
|
|
337
|
+
// ── Enter ────────────────────────────────────────────────────────────────
|
|
301
338
|
if (key === KEYS.ENTER) {
|
|
302
339
|
const col = cols[colIdx];
|
|
303
340
|
if (col === 'type') { typeMenuOpen = true; typeMenuIdx = 0; render(); return; }
|
|
@@ -308,9 +345,9 @@ export async function fieldsTable(initialRows, cols, widths, title) {
|
|
|
308
345
|
return;
|
|
309
346
|
}
|
|
310
347
|
|
|
311
|
-
// Text editing
|
|
348
|
+
// ── Text editing ─────────────────────────────────────────────────────────
|
|
312
349
|
const col = cols[colIdx];
|
|
313
|
-
if (col
|
|
350
|
+
if (BOOL_COLS.has(col) || CYCLE_COLS[col]) return; // not typed
|
|
314
351
|
|
|
315
352
|
if (key === KEYS.BACKSPACE) {
|
|
316
353
|
rows[rowIdx][col] = String(rows[rowIdx][col] || '').slice(0, -1);
|
|
@@ -318,7 +355,6 @@ export async function fieldsTable(initialRows, cols, widths, title) {
|
|
|
318
355
|
return;
|
|
319
356
|
}
|
|
320
357
|
|
|
321
|
-
// Printable character
|
|
322
358
|
if (key.length === 1 && key >= ' ') {
|
|
323
359
|
rows[rowIdx][col] = (rows[rowIdx][col] || '') + key;
|
|
324
360
|
render();
|
package/src/utils/naming.js
CHANGED
|
@@ -27,15 +27,25 @@ export function parseEntityPath(input) {
|
|
|
27
27
|
|
|
28
28
|
let raw = input.trim();
|
|
29
29
|
|
|
30
|
-
// Handle
|
|
30
|
+
// Handle dot notation ONLY when the part before the dot could be a module name
|
|
31
|
+
// and the part after is NOT a schema keyword (dbo, etc.) — i.e. "rh.employee" → "rh/employee"
|
|
32
|
+
// BUT "POLE.dbo" must NOT be rewritten — POLE is the entity, dbo is the schema.
|
|
33
|
+
// Rule: rewrite "x.y" only when x contains no uppercase and y is not a known schema.
|
|
34
|
+
const SCHEMA_NAMES = new Set(['dbo', 'dbo2', 'hr', 'rh', 'stk', 'fin', 'crm', 'adm', 'sec', 'shared']);
|
|
31
35
|
if (!raw.includes('/') && raw.includes('.') && raw.split('.').length === 2) {
|
|
32
|
-
const [
|
|
33
|
-
|
|
36
|
+
const [left, right] = raw.split('.');
|
|
37
|
+
const looksLikeSchema = SCHEMA_NAMES.has(right.toLowerCase()) || /^[a-z]+$/.test(right);
|
|
38
|
+
const looksLikeModule = /^[a-z][a-z0-9_-]*$/.test(left);
|
|
39
|
+
// Only rewrite if left looks like a lowercase module AND right does NOT look like a schema
|
|
40
|
+
if (looksLikeModule && !looksLikeSchema) {
|
|
41
|
+
raw = `${left}/${right}`;
|
|
42
|
+
}
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
// No slash at all → shared
|
|
37
46
|
if (!raw.includes('/')) {
|
|
38
|
-
|
|
47
|
+
const _e = toLower(raw);
|
|
48
|
+
return { module: 'shared', entity: _e, table: _e, schema: 'dbo' };
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
// Split on /
|
|
@@ -49,20 +59,34 @@ export function parseEntityPath(input) {
|
|
|
49
59
|
const dotIdx = rest.indexOf('.');
|
|
50
60
|
let entity, schema;
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
// Also parse optional table alias: entity:tableName
|
|
63
|
+
// e.g. rh/agent:agentx.dbo → entity=agent, table=agentx, schema=dbo
|
|
64
|
+
let tableAlias = null;
|
|
65
|
+
if (rest.includes(':')) {
|
|
66
|
+
const colonIdx = rest.indexOf(':');
|
|
67
|
+
tableAlias = rest.slice(colonIdx + 1).split('.')[0]; // strip schema part
|
|
68
|
+
// rewrite rest without the alias for further parsing
|
|
69
|
+
// keep the .schema part after the alias if any
|
|
70
|
+
const afterColon = rest.slice(colonIdx + 1);
|
|
71
|
+
const schemaInAlias = afterColon.includes('.') ? afterColon.slice(afterColon.indexOf('.')) : '';
|
|
72
|
+
// rest becomes "entity.schema"
|
|
73
|
+
// rest = rest.slice(0, colonIdx) + schemaInAlias; — handled below
|
|
74
|
+
}
|
|
75
|
+
const cleanRest = rest.replace(/:([^.]+)/, ''); // strip :alias for entity/schema parsing
|
|
76
|
+
|
|
77
|
+
const dotIdx2 = cleanRest.indexOf('.');
|
|
78
|
+
let entity, schema;
|
|
79
|
+
if (dotIdx2 === -1) {
|
|
80
|
+
entity = toLower(cleanRest);
|
|
55
81
|
schema = module === 'shared' ? 'dbo' : module;
|
|
56
82
|
} else {
|
|
57
|
-
entity = toLower(
|
|
58
|
-
const schemaPart =
|
|
59
|
-
|
|
60
|
-
// Trailing dot → schema = module
|
|
61
|
-
schema = module === 'shared' ? 'dbo' : module;
|
|
62
|
-
} else {
|
|
63
|
-
schema = toLower(schemaPart);
|
|
64
|
-
}
|
|
83
|
+
entity = toLower(cleanRest.slice(0, dotIdx2));
|
|
84
|
+
const schemaPart = cleanRest.slice(dotIdx2 + 1);
|
|
85
|
+
schema = schemaPart === '' ? (module === 'shared' ? 'dbo' : module) : toLower(schemaPart);
|
|
65
86
|
}
|
|
87
|
+
const table = tableAlias ? toLower(tableAlias) : entity;
|
|
88
|
+
// (keep legacy variable for code below that uses dotIdx)
|
|
89
|
+
const dotIdx = dotIdx2;
|
|
66
90
|
|
|
67
91
|
if (!isValidName(entity)) {
|
|
68
92
|
throw new Error(`Invalid entity name "${entity}". Use alphanumeric, hyphens, underscores only.`);
|
|
@@ -71,7 +95,7 @@ export function parseEntityPath(input) {
|
|
|
71
95
|
throw new Error(`Invalid module name "${module}".`);
|
|
72
96
|
}
|
|
73
97
|
|
|
74
|
-
return { module, entity, schema };
|
|
98
|
+
return { module, entity, table: table || entity, schema };
|
|
75
99
|
}
|
|
76
100
|
|
|
77
101
|
/**
|