@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 +1 -1
- package/src/generators/entity.js +84 -54
- package/src/generators/menu.js +26 -23
- 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 +6 -2
- package/src/tui/engine.js +2 -1
- package/src/utils/naming.js +61 -43
package/package.json
CHANGED
package/src/generators/entity.js
CHANGED
|
@@ -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
|
|
4
|
+
import { writeFile, writeJson, exists, assertProjectRoot } from '../utils/fs.js';
|
|
5
5
|
import { log } from '../utils/log.js';
|
|
6
|
-
import { textInput, optionsLine, fieldsTable
|
|
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`,
|
|
13
|
-
{ field: `${entityName}_lib`, type: 'varchar',
|
|
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
|
|
33
|
-
const dbAlias
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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()
|
|
81
|
-
export async function getById(${pkField})
|
|
82
|
-
export async function add(data)
|
|
83
|
-
export async function update(${pkField}, data)
|
|
84
|
-
export async function 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(
|
|
134
|
-
|
|
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(
|
|
138
|
-
|
|
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(
|
|
142
|
-
|
|
162
|
+
await writeFile(
|
|
163
|
+
path.join(backendBase, 'ipc.json'),
|
|
164
|
+
buildIpcJson({ module, entity, schema }),
|
|
165
|
+
{ dryRun }
|
|
166
|
+
);
|
|
143
167
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
150
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
199
|
-
|
|
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:
|
|
234
|
+
// ── --default: instant generation ────────────────────────────────────────
|
|
205
235
|
if (useDefault) {
|
|
206
236
|
await writeEntity({
|
|
207
|
-
module, entity, schema,
|
|
208
|
-
fields:
|
|
237
|
+
module, entity, table, schema,
|
|
238
|
+
fields: defaultFillableRows(entity),
|
|
209
239
|
auditFields: DEFAULT_AUDIT_ROWS,
|
|
210
|
-
pkField,
|
|
211
|
-
opts:
|
|
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
|
-
//
|
|
245
|
-
const pkRow
|
|
246
|
-
const resolvedPk = pkRow ? pkRow.field :
|
|
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:
|
|
279
|
+
module, entity, table, schema,
|
|
280
|
+
fields: fillableRows,
|
|
251
281
|
auditFields: auditRows,
|
|
252
|
-
pkField:
|
|
282
|
+
pkField: resolvedPk,
|
|
253
283
|
opts,
|
|
254
284
|
dryRun,
|
|
255
285
|
});
|
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,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',
|
|
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', '
|
|
68
|
-
{ label: 16, route: 20, icon:
|
|
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}`,
|
|
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
|
@@ -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 === '
|
|
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
|
|
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.
|
package/src/utils/naming.js
CHANGED
|
@@ -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
|
|
19
|
-
* rh/employee
|
|
20
|
-
* rh/employee.dbo
|
|
21
|
-
* rh/employee.hr
|
|
22
|
-
* rh/employee.
|
|
23
|
-
* employee
|
|
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
|
-
//
|
|
35
|
+
// ── Old "module.entity" compat (only if left is strictly lowercase) ────────
|
|
31
36
|
if (!raw.includes('/') && raw.includes('.') && raw.split('.').length === 2) {
|
|
32
|
-
const [
|
|
33
|
-
|
|
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
|
|
46
|
+
// ── No slash → shared module ───────────────────────────────────────────────
|
|
37
47
|
if (!raw.includes('/')) {
|
|
38
|
-
|
|
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
|
-
//
|
|
42
|
-
const slashIdx
|
|
43
|
-
const
|
|
44
|
-
const rest
|
|
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
|
-
|
|
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
|
-
//
|
|
49
|
-
const
|
|
80
|
+
// ── Entity and schema ──────────────────────────────────────────────────────
|
|
81
|
+
const dotPos = cleanRest.indexOf('.');
|
|
50
82
|
let entity, schema;
|
|
51
83
|
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
entity = toLower(rest);
|
|
84
|
+
if (dotPos === -1) {
|
|
85
|
+
entity = toLower(cleanRest);
|
|
55
86
|
schema = module === 'shared' ? 'dbo' : module;
|
|
56
87
|
} else {
|
|
57
|
-
entity
|
|
58
|
-
const schemaPart =
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
}
|