@cosider.construction/eapp 1.0.3 → 1.0.5

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": "@cosider.construction/eapp",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Project-local CLI for Electron + Vue ERP projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,16 +1,16 @@
1
1
  import path from 'path';
2
2
  import chalk from 'chalk';
3
3
  import { parseEntityPath, ipcRoute, toUpper, toLower, toPascal } from '../utils/naming.js';
4
- import { writeFile, writeJson, exists, assertProjectRoot, loadEntityRegistry } from '../utils/fs.js';
4
+ import { writeFile, writeJson, exists, assertProjectRoot } from '../utils/fs.js';
5
5
  import { log } from '../utils/log.js';
6
- import { textInput, optionsLine, fieldsTable, radioLine } from '../tui/engine.js';
6
+ import { textInput, optionsLine, fieldsTable } from '../tui/engine.js';
7
7
 
8
8
  // ─── Default fields ───────────────────────────────────────────────────────────
9
9
 
10
10
  function defaultFillableRows(entityName) {
11
11
  return [
12
- { field: `${entityName}_id`, type: 'int', values: '', pk: true, fk: false, group: '' },
13
- { field: `${entityName}_lib`, type: 'varchar', values: '', pk: false, fk: false, group: '' },
12
+ { field: `${entityName}_id`, type: 'int', values: '', pk: true, fk: false, group: '' },
13
+ { field: `${entityName}_lib`, type: 'varchar', values: '', pk: false, fk: false, group: '' },
14
14
  ];
15
15
  }
16
16
 
@@ -28,12 +28,15 @@ 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);
33
- const dbAlias = schema === 'dbo' ? 'dbo' : schema;
34
- const importPath = module === 'shared' ? '../../../engine/db.js' : '../../engine/db.js';
31
+ function buildModelJs({ module, entity, table, schema, fields, auditFields, pkField }) {
32
+ const TABLE = toUpper(table || entity);
33
+ const dbAlias = schema === 'dbo' ? 'dbo' : schema;
34
+ // import path: shared entities are 3 levels deep (shared/ENTITY/model.js engine/db.js)
35
+ // module entities are also 3 levels deep (modules/MOD/ENTITY/model.js → engine/db.js)
36
+ const importPath = module === 'shared'
37
+ ? '../../../engine/db.js'
38
+ : '../../../engine/db.js';
35
39
 
36
- const allFields = [...fields, ...auditFields];
37
40
  const fillable = fields.filter(f => !f.pk);
38
41
  const insertCols = fillable.map(f => f.field).join(', ');
39
42
  const insertVals = fillable.map(f => `@${f.field}`).join(', ');
@@ -44,6 +47,7 @@ function buildModelJs({ module, entity, schema, fields, auditFields, pkField })
44
47
  // Table : ${TABLE}
45
48
  // Schema : ${schema}
46
49
  // PK : ${pkField}
50
+ // The scoped DB instance automatically prepends [database].[${schema}] to table names.
47
51
 
48
52
  export async function getAll() {
49
53
  return db.query('SELECT * FROM ${TABLE}');
@@ -56,20 +60,20 @@ export async function getById(${pkField}) {
56
60
  export async function add(data) {
57
61
  const { ${fillable.map(f => f.field).join(', ') || '/* fields */'} } = data;
58
62
  return db.execute(
59
- 'INSERT INTO ${TABLE} (${insertCols || '/* cols */'}) VALUES (${insertVals || '/* vals */'})',
63
+ \`INSERT INTO ${TABLE} (${insertCols || '/* cols */'}) VALUES (${insertVals || '/* vals */'})\`,
60
64
  data
61
65
  );
62
66
  }
63
67
 
64
68
  export async function update(${pkField}, data) {
65
69
  return db.execute(
66
- 'UPDATE ${TABLE} SET ${updateSet || '/* set */'} WHERE ${pkField} = @${pkField}',
70
+ \`UPDATE ${TABLE} SET ${updateSet || '/* set clause */'} WHERE ${pkField} = @${pkField}\`,
67
71
  { ...data, ${pkField} }
68
72
  );
69
73
  }
70
74
 
71
75
  export async function remove(${pkField}) {
72
- return db.execute('DELETE FROM ${TABLE} WHERE ${pkField} = @${pkField}', { ${pkField} });
76
+ return db.execute(\`DELETE FROM ${TABLE} WHERE ${pkField} = @${pkField}\`, { ${pkField} });
73
77
  }
74
78
  `;
75
79
  }
@@ -77,11 +81,11 @@ export async function remove(${pkField}) {
77
81
  function buildControllerJs({ pkField }) {
78
82
  return `import * as model from './model.js';
79
83
 
80
- export async function getAll() { return model.getAll(); }
81
- export async function getById(${pkField}) { return model.getById(${pkField}); }
82
- export async function add(data) { return model.add(data); }
83
- export async function update(${pkField}, data) { return model.update(${pkField}, data); }
84
- export async function remove(${pkField}) { return model.remove(${pkField}); }
84
+ export async function getAll() { return model.getAll(); }
85
+ export async function getById(${pkField}) { return model.getById(${pkField}); }
86
+ export async function add(data) { return model.add(data); }
87
+ export async function update(${pkField}, data) { return model.update(${pkField}, data); }
88
+ export async function remove(${pkField}) { return model.remove(${pkField}); }
85
89
  `;
86
90
  }
87
91
 
@@ -115,14 +119,25 @@ export default new ${ClassName}();
115
119
  `;
116
120
  }
117
121
 
118
- function buildMetaJson({ module, entity, schema, fields, auditFields, pkField }) {
119
- return JSON.stringify({ module, entity, schema, fields, auditFields, pkField }, null, 2);
122
+ function buildMetaJson({ module, entity, table, schema, fields, auditFields, pkField }) {
123
+ return JSON.stringify({ module, entity, table, schema, fields, auditFields, pkField }, null, 2);
120
124
  }
121
125
 
122
126
  // ─── Write entity files ───────────────────────────────────────────────────────
123
127
 
124
- async function writeEntity({ module, entity, schema, fields, auditFields, pkField, opts, dryRun }) {
128
+ async function writeEntity({ module, entity, table, schema, fields, auditFields, pkField, opts, dryRun }) {
125
129
  const cwd = process.cwd();
130
+
131
+ // Display path
132
+ const displayPath = `${module}/${entity}` +
133
+ (table !== entity ? `:${table}` : '') +
134
+ `.${schema}`;
135
+ console.log('');
136
+ console.log(chalk.bold.white(' Entity ') + chalk.cyan(displayPath));
137
+ if (table !== entity) {
138
+ console.log(chalk.dim(` DB table name: ${toUpper(table)}`));
139
+ }
140
+
126
141
  const backendBase = module === 'shared'
127
142
  ? path.join(cwd, 'src', 'backend', 'shared', toUpper(entity))
128
143
  : path.join(cwd, 'src', 'backend', 'modules', toUpper(module), toUpper(entity));
@@ -130,28 +145,43 @@ async function writeEntity({ module, entity, schema, fields, auditFields, pkFiel
130
145
  const o = opts || { model: true, controller: true, crud: true, api: true, ipc: true };
131
146
 
132
147
  if (o.model) {
133
- await writeFile(path.join(backendBase, 'model.js'),
134
- buildModelJs({ module, entity, schema, fields, auditFields, pkField }), { dryRun });
148
+ await writeFile(
149
+ path.join(backendBase, 'model.js'),
150
+ buildModelJs({ module, entity, table, schema, fields, auditFields, pkField }),
151
+ { dryRun }
152
+ );
135
153
  }
136
154
  if (o.controller) {
137
- await writeFile(path.join(backendBase, 'controller.js'),
138
- buildControllerJs({ pkField }), { dryRun });
155
+ await writeFile(
156
+ path.join(backendBase, 'controller.js'),
157
+ buildControllerJs({ pkField }),
158
+ { dryRun }
159
+ );
139
160
  }
140
161
  if (o.ipc) {
141
- await writeFile(path.join(backendBase, 'ipc.json'),
142
- buildIpcJson({ module, entity, schema }), { dryRun });
162
+ await writeFile(
163
+ path.join(backendBase, 'ipc.json'),
164
+ buildIpcJson({ module, entity, schema }),
165
+ { dryRun }
166
+ );
143
167
  }
144
- // Always write meta so registry can read it
145
- await writeJson(path.join(backendBase, 'entity.meta.json'),
146
- JSON.parse(buildMetaJson({ module, entity, schema, fields, auditFields, pkField })), { dryRun });
168
+
169
+ // Always write meta — entity registry reads this
170
+ await writeJson(
171
+ path.join(backendBase, 'entity.meta.json'),
172
+ { module, entity, table, schema, fields, auditFields, pkField },
173
+ { dryRun }
174
+ );
147
175
 
148
176
  if (o.api && o.ipc) {
149
- const apiPath = path.join(cwd, 'src', 'frontend', 'api', 'modules',
150
- `${toLower(module)}.${toLower(entity)}.api.js`);
177
+ const apiPath = path.join(
178
+ cwd, 'src', 'frontend', 'api', 'modules',
179
+ `${toLower(module)}.${toLower(entity)}.api.js`
180
+ );
151
181
  await writeFile(apiPath, buildApiJs({ module, entity }), { dryRun });
152
182
  }
153
183
 
154
- log.success(`Entity ${chalk.cyan(module + '/' + entity + '.' + schema)} generated.`);
184
+ log.success(`Entity ${chalk.cyan(displayPath)} generated.`);
155
185
  }
156
186
 
157
187
  // ─── Main generator ───────────────────────────────────────────────────────────
@@ -167,10 +197,12 @@ export async function generateEntity(args, rawArgs) {
167
197
  console.log('');
168
198
  console.log(chalk.bold.cyan(' eapp entity') + chalk.dim(' <module/entity.schema>'));
169
199
  console.log('');
170
- console.log(chalk.dim(' /employee → shared/employee.dbo'));
200
+ console.log(chalk.dim(' /employee → shared, employee, dbo'));
171
201
  console.log(chalk.dim(' rh/employee → rh module, rh schema'));
172
202
  console.log(chalk.dim(' rh/employee.dbo → rh module, dbo schema'));
173
- console.log(chalk.dim(' rh/employee.hr → rh module, custom schema hr'));
203
+ console.log(chalk.dim(' rh/employee.hr → rh module, custom schema'));
204
+ console.log(chalk.dim(' POLE.dbo → shared, pole entity, dbo schema'));
205
+ console.log(chalk.dim(' rh/agent:AGENTX → rh module, entity=agent, table=AGENTX'));
174
206
  console.log('');
175
207
  }
176
208
 
@@ -186,8 +218,8 @@ export async function generateEntity(args, rawArgs) {
186
218
  catch (e) { log.error(e.message); process.exit(1); }
187
219
  }
188
220
 
189
- const { module, entity, schema } = parsed;
190
- const pkField = `${entity}_id`;
221
+ const { module, entity, table, schema } = parsed;
222
+ const defaultPk = `${entity}_id`;
191
223
 
192
224
  // ── Check if exists ───────────────────────────────────────────────────────
193
225
  const cwd = process.cwd();
@@ -195,20 +227,18 @@ export async function generateEntity(args, rawArgs) {
195
227
  ? path.join(cwd, 'src', 'backend', 'shared', toUpper(entity))
196
228
  : path.join(cwd, 'src', 'backend', 'modules', toUpper(module), toUpper(entity));
197
229
 
198
- const entityExists = await exists(backendBase);
199
- if (entityExists) {
200
- log.info(`Entity ${chalk.cyan(module + '/' + entity)} already exists.`);
201
- log.dim(' Regenerating files — existing logic will be preserved in model/controller.');
230
+ if (await exists(backendBase)) {
231
+ log.info(`Entity ${chalk.cyan(module + '/' + entity)} already exists — regenerating.`);
202
232
  }
203
233
 
204
- // ── --default: skip all prompts ───────────────────────────────────────────
234
+ // ── --default: instant generation ────────────────────────────────────────
205
235
  if (useDefault) {
206
236
  await writeEntity({
207
- module, entity, schema,
208
- fields: defaultFillableRows(entity),
237
+ module, entity, table, schema,
238
+ fields: defaultFillableRows(entity),
209
239
  auditFields: DEFAULT_AUDIT_ROWS,
210
- pkField,
211
- opts: { model: true, controller: true, crud: true, api: true, ipc: true },
240
+ pkField: defaultPk,
241
+ opts: { model: true, controller: true, crud: true, api: true, ipc: true },
212
242
  dryRun,
213
243
  });
214
244
  return;
@@ -225,7 +255,7 @@ export async function generateEntity(args, rawArgs) {
225
255
  { key: 'ipc', label: 'IPC', default: true },
226
256
  ]);
227
257
 
228
- // ── Fillable fields ───────────────────────────────────────────────────────
258
+ // ── Fillable fields table ─────────────────────────────────────────────────
229
259
  const fillableRows = await fieldsTable(
230
260
  defaultFillableRows(entity),
231
261
  FILLABLE_COLS,
@@ -233,23 +263,23 @@ export async function generateEntity(args, rawArgs) {
233
263
  'Fillable Fields (+ add - remove ← → cols ↑ ↓ rows space pk/fk enter done)'
234
264
  );
235
265
 
236
- // ── Audit fields ──────────────────────────────────────────────────────────
266
+ // ── Audit fields table ────────────────────────────────────────────────────
237
267
  const auditRows = await fieldsTable(
238
268
  [...DEFAULT_AUDIT_ROWS],
239
269
  AUDIT_COLS,
240
270
  AUDIT_WIDTHS,
241
- 'Audit Fields (non-fillable) (- to remove unwanted)'
271
+ 'Audit Fields (non-fillable) (- to remove unwanted enter done)'
242
272
  );
243
273
 
244
- // Find PK
245
- const pkRow = fillableRows.find(r => r.pk);
246
- const resolvedPk = pkRow ? pkRow.field : pkField;
274
+ // Resolve PK from fields
275
+ const pkRow = fillableRows.find(r => r.pk);
276
+ const resolvedPk = pkRow ? pkRow.field : defaultPk;
247
277
 
248
278
  await writeEntity({
249
- module, entity, schema,
250
- fields: fillableRows,
279
+ module, entity, table, schema,
280
+ fields: fillableRows,
251
281
  auditFields: auditRows,
252
- pkField: resolvedPk,
282
+ pkField: resolvedPk,
253
283
  opts,
254
284
  dryRun,
255
285
  });
@@ -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
- route: item.route || '',
17
- icon: item.icon || '',
18
- parent: parentLabel,
19
- permission: item.permission || '',
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.parent);
32
- const children = rows.filter(r => r.parent);
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, route: r.route, icon: r.icon };
36
- if (r.permission) item.permission = r.permission;
37
- const kids = children.filter(c => c.parent === r.label);
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, route: c.route, icon: c.icon,
41
- ...(c.permission ? { permission: c.permission } : {}),
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,13 +62,13 @@ 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', route: '', icon: '', parent: '', permission: '' });
65
+ existing_items.push({ label: 'Action', icon: '', path: '', route: '', dot: 'off', nbr: '' });
63
66
  }
64
67
  console.log('');
65
68
  const rows = await fieldsTable(
66
69
  existing_items,
67
- ['label', 'route', 'icon', 'parent', 'permission'],
68
- { label: 16, route: 20, icon: 6, parent: 14, permission: 20 },
70
+ ['label', 'route', 'icon', 'path', 'dot', 'nbr'],
71
+ { label: 16, route: 20, icon: 5, path: 14, dot: 5, nbr: 6 },
69
72
  'hMenu items (parent = dropdown group label, empty = top level)'
70
73
  );
71
74
  const menu = { route: existing?.route || '', hMenu: { style: 'classic', items: nestItems(rows) } };
@@ -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}`, icon: '', parent: '', permission: '' });
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', 'route', 'icon', 'parent', 'permission'],
194
- { label: 16, route: 20, icon: 6, parent: 14, permission: 20 },
195
- 'hMenu (parent = dropdown group label, empty = top level enter done)'
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}`, icon: '', parent: '', permission: '' });
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', 'route', 'icon', 'parent', 'permission'],
209
- { label: 16, route: 20, icon: 6, parent: 14, permission: 20 },
210
- 'vMenu (parent = collapsible group label, empty = top level enter done)'
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
@@ -121,7 +121,7 @@ export async function generateModule(args, rawArgs) {
121
121
  return {
122
122
  module: lower,
123
123
  layout: `${toPascal(lower)}Layout`,
124
- vmenu: 'own',
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`, vmenu: 'own', home: 'on', grid: 'on' });
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', 'vmenu', 'home', 'grid'],
150
- { module: 14, layout: 18, vmenu: 8, home: 6, grid: 6 },
151
- 'Modules (edit layout name, vmenu: own|share, home/grid: on|off enter to save)'
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 hasGrid = row.grid !== 'off';
159
- const ownVMenu = row.vmenu !== 'share';
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
+ }
@@ -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 cols space menu toggle enter done)'
352
+ 'Components (+ add - remove \u2190 \u2192 cols space menu toggle enter done)'
353
353
  );
354
354
 
355
- const hasRow = compRows.some(r => r.render === 'row' || r.render === 'table');
356
- const hasCard = compRows.some(r => r.render === 'card');
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
@@ -41,11 +41,12 @@ Options:
41
41
 
42
42
  const MAIN_MENU = [
43
43
  { label: 'Entity', value: 'entity' },
44
+ { label: 'Pivot', value: 'pivot' },
44
45
  { label: 'View', value: 'view' },
45
46
  { label: 'Module', value: 'module' },
46
47
  { label: 'Menu', value: 'menu' },
48
+ { label: 'Sql', value: 'sql' },
47
49
  { label: 'Config', value: 'config' },
48
- { label: 'Pivot', value: 'pivot' },
49
50
  { label: 'Layout', value: 'layout' },
50
51
  { label: 'List', value: 'list' },
51
52
  { label: 'Quit', value: 'quit' },
@@ -72,7 +73,10 @@ async function launchInteractive() {
72
73
  break;
73
74
  }
74
75
 
75
- if (chosen === 'list') {
76
+ if (chosen === 'sql') {
77
+ const { generateSql } = await import('./generators/sql.js');
78
+ await generateSql([], []);
79
+ } else if (chosen === 'list') {
76
80
  await listModules();
77
81
  console.log('');
78
82
  await listEntities([]);
package/src/tui/engine.js CHANGED
@@ -161,8 +161,9 @@ const BOOL_COLS = new Set(['pk', 'fk']);
161
161
  // Columns where space cycles between string values
162
162
  const CYCLE_COLS = {
163
163
  vmenu: ['own', 'share'],
164
+ hmenu: ['own', 'share', 'none'],
164
165
  home: ['on', 'off'],
165
- grid: ['on', 'off'],
166
+ // 'grid' is free-text (NxM), not cycled
166
167
  };
167
168
 
168
169
  // Strip ANSI escape codes to get the true printable length of a string.
@@ -15,69 +15,89 @@ export const isValidName = (s) => /^[a-zA-Z0-9_-]+$/.test(s);
15
15
  /**
16
16
  * Parse "module/entity.schema" format
17
17
  *
18
- * /employee { module: 'shared', entity: 'employee', schema: 'dbo' }
19
- * rh/employee { module: 'rh', entity: 'employee', schema: 'rh' }
20
- * rh/employee.dbo { module: 'rh', entity: 'employee', schema: 'dbo' }
21
- * rh/employee.hr { module: 'rh', entity: 'employee', schema: 'hr' }
22
- * rh/employee. { module: 'rh', entity: 'employee', schema: 'rh' } (dot alone = module)
23
- * employee { module: 'shared', entity: 'employee', schema: 'dbo' } (no slash = shared)
18
+ * /employee → shared, employee, dbo
19
+ * rh/employee → rh, employee, rh
20
+ * rh/employee.dbo → rh, employee, dbo
21
+ * rh/employee.hr → rh, employee, hr
22
+ * rh/employee. → rh, employee, rh (dot alone = module schema)
23
+ * employee → shared, employee, dbo
24
+ * POLE.dbo → shared, pole, dbo (uppercase entity with schema)
25
+ * rh/agent:AGENTX.dbo → rh, agent, dbo, table=agentx
26
+ *
27
+ * OLD compat dot notation: rh.employee → rh/employee
28
+ * BUT: POLE.dbo must NOT rewrite — left has uppercase so it's not a module name
24
29
  */
25
30
  export function parseEntityPath(input) {
26
31
  if (!input) throw new Error('No entity path provided.');
27
32
 
28
33
  let raw = input.trim();
29
34
 
30
- // Handle old dot notation for backwards compat: rh.employee rh/employee
35
+ // ── Old "module.entity" compat (only if left is strictly lowercase) ────────
31
36
  if (!raw.includes('/') && raw.includes('.') && raw.split('.').length === 2) {
32
- const [mod, ent] = raw.split('.');
33
- raw = `${mod}/${ent}`;
37
+ const [left, right] = raw.split('.');
38
+ const leftIsLowercaseModule = /^[a-z][a-z0-9_-]*$/.test(left);
39
+ const rightIsSchemaKeyword = /^(dbo\d*|[a-z]{1,6})$/.test(right.toLowerCase());
40
+ if (leftIsLowercaseModule && !rightIsSchemaKeyword) {
41
+ raw = `${left}/${right}`;
42
+ }
43
+ // Otherwise treat as entity.schema without module (e.g. POLE.dbo, employee.dbo)
34
44
  }
35
45
 
36
- // No slash at all shared
46
+ // ── No slash shared module ───────────────────────────────────────────────
37
47
  if (!raw.includes('/')) {
38
- return { module: 'shared', entity: toLower(raw), schema: 'dbo' };
48
+ const dotPos = raw.indexOf('.');
49
+ let ent, sch;
50
+ if (dotPos !== -1) {
51
+ ent = toLower(raw.slice(0, dotPos));
52
+ sch = raw.slice(dotPos + 1).toLowerCase() || 'dbo';
53
+ } else {
54
+ ent = toLower(raw);
55
+ sch = 'dbo';
56
+ }
57
+ if (!isValidName(ent)) throw new Error(`Invalid entity name "${ent}".`);
58
+ return { module: 'shared', entity: ent, table: ent, schema: sch };
39
59
  }
40
60
 
41
- // Split on /
42
- const slashIdx = raw.indexOf('/');
43
- const modulePart = raw.slice(0, slashIdx); // may be empty → shared
44
- const rest = raw.slice(slashIdx + 1); // entity.schema or entity
61
+ // ── Has slash ─────────────────────────────────────────────────────────────
62
+ const slashIdx = raw.indexOf('/');
63
+ const modPart = raw.slice(0, slashIdx);
64
+ const rest = raw.slice(slashIdx + 1);
65
+ const module = modPart === '' ? 'shared' : toLower(modPart);
45
66
 
46
- const module = modulePart === '' ? 'shared' : toLower(modulePart);
67
+ // ── Optional table alias: entity:TABLE.schema ──────────────────────────────
68
+ let tableAlias = null;
69
+ let cleanRest = rest;
70
+ const colonPos = rest.indexOf(':');
71
+ if (colonPos !== -1) {
72
+ const beforeColon = rest.slice(0, colonPos);
73
+ const afterColon = rest.slice(colonPos + 1);
74
+ const aliasDot = afterColon.indexOf('.');
75
+ tableAlias = toLower(aliasDot !== -1 ? afterColon.slice(0, aliasDot) : afterColon);
76
+ const schemaRest = aliasDot !== -1 ? afterColon.slice(aliasDot) : '';
77
+ cleanRest = beforeColon + schemaRest;
78
+ }
47
79
 
48
- // Split entity and schema on .
49
- const dotIdx = rest.indexOf('.');
80
+ // ── Entity and schema ──────────────────────────────────────────────────────
81
+ const dotPos = cleanRest.indexOf('.');
50
82
  let entity, schema;
51
83
 
52
- if (dotIdx === -1) {
53
- // No dot → schema = module (or dbo for shared)
54
- entity = toLower(rest);
84
+ if (dotPos === -1) {
85
+ entity = toLower(cleanRest);
55
86
  schema = module === 'shared' ? 'dbo' : module;
56
87
  } else {
57
- entity = toLower(rest.slice(0, dotIdx));
58
- const schemaPart = rest.slice(dotIdx + 1);
59
- if (schemaPart === '') {
60
- // Trailing dot → schema = module
61
- schema = module === 'shared' ? 'dbo' : module;
62
- } else {
63
- schema = toLower(schemaPart);
64
- }
88
+ entity = toLower(cleanRest.slice(0, dotPos));
89
+ const schemaPart = cleanRest.slice(dotPos + 1);
90
+ schema = schemaPart === '' ? (module === 'shared' ? 'dbo' : module) : toLower(schemaPart);
65
91
  }
66
92
 
67
- if (!isValidName(entity)) {
68
- throw new Error(`Invalid entity name "${entity}". Use alphanumeric, hyphens, underscores only.`);
69
- }
70
- if (module !== 'shared' && !isValidName(module)) {
71
- throw new Error(`Invalid module name "${module}".`);
72
- }
93
+ if (!isValidName(entity)) throw new Error(`Invalid entity name "${entity}".`);
94
+ if (module !== 'shared' && !isValidName(module)) throw new Error(`Invalid module name "${module}".`);
73
95
 
74
- return { module, entity, schema };
96
+ return { module, entity, table: tableAlias || entity, schema };
75
97
  }
76
98
 
77
99
  /**
78
- * Derive IPC route
79
- * shared + client → shared.CLIENT
80
- * rh + employee → modules.RH.EMPLOYEE
100
+ * IPC route: shared.CLIENT or modules.RH.EMPLOYEE
81
101
  */
82
102
  export function ipcRoute(module, entity) {
83
103
  if (module === 'shared') return `shared.${toUpper(entity)}`;
@@ -85,11 +105,9 @@ export function ipcRoute(module, entity) {
85
105
  }
86
106
 
87
107
  /**
88
- * Derive the backend directory path for an entity
108
+ * Backend directory for an entity
89
109
  */
90
110
  export function backendEntityDir(cwd, module, entity) {
91
- if (module === 'shared') {
92
- return `${cwd}/src/backend/shared/${toUpper(entity)}`;
93
- }
111
+ if (module === 'shared') return `${cwd}/src/backend/shared/${toUpper(entity)}`;
94
112
  return `${cwd}/src/backend/modules/${toUpper(module)}/${toUpper(entity)}`;
95
113
  }