aegisnode 0.0.1
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/LICENSE +21 -0
- package/README.md +2461 -0
- package/bin/aegisnode.js +9 -0
- package/package.json +56 -0
- package/scripts/smoke-test.js +1831 -0
- package/src/cli/commands/createapp.js +191 -0
- package/src/cli/commands/doctor.js +199 -0
- package/src/cli/commands/generate.js +266 -0
- package/src/cli/commands/runserver.js +17 -0
- package/src/cli/commands/startproject.js +72 -0
- package/src/cli/commands/updatedeps.js +355 -0
- package/src/cli/index.js +151 -0
- package/src/cli/utils/fs.js +53 -0
- package/src/cli/utils/project.js +67 -0
- package/src/cli/utils/scaffolds.js +596 -0
- package/src/index.js +20 -0
- package/src/runtime/auth.js +2291 -0
- package/src/runtime/cache.js +37 -0
- package/src/runtime/config.js +482 -0
- package/src/runtime/container.js +43 -0
- package/src/runtime/database.js +195 -0
- package/src/runtime/events.js +33 -0
- package/src/runtime/helpers.js +575 -0
- package/src/runtime/kernel.js +3713 -0
- package/src/runtime/loaders.js +46 -0
- package/src/runtime/logger.js +56 -0
- package/src/runtime/mail.js +225 -0
- package/src/runtime/upload.js +272 -0
- package/src/runtime/views/default-install.ejs +183 -0
- package/src/runtime/views/default-maintenance.ejs +148 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { ensureDir, ensureValidName, exists, normalizeMountPath, writeFile } from '../utils/fs.js';
|
|
5
|
+
import { resolveProjectRoot } from '../utils/project.js';
|
|
6
|
+
import {
|
|
7
|
+
renderAppRoutes,
|
|
8
|
+
renderAppViewsFile,
|
|
9
|
+
renderAppModelsFile,
|
|
10
|
+
renderAppValidatorsFile,
|
|
11
|
+
renderAppServicesFile,
|
|
12
|
+
renderAppSubscribersFile,
|
|
13
|
+
renderAppModelTest,
|
|
14
|
+
renderAppValidatorTest,
|
|
15
|
+
renderAppServiceTest,
|
|
16
|
+
renderAppRoutesTest,
|
|
17
|
+
renderSettingsApps,
|
|
18
|
+
} from '../utils/scaffolds.js';
|
|
19
|
+
|
|
20
|
+
const APPS_START = '// AEGIS_APPS_START';
|
|
21
|
+
const APPS_END = '// AEGIS_APPS_END';
|
|
22
|
+
|
|
23
|
+
function renderAppEntries(apps) {
|
|
24
|
+
return apps
|
|
25
|
+
.map((app) => ` { name: ${JSON.stringify(app.name)}, mount: ${JSON.stringify(app.mount)} },`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toImportName(appName) {
|
|
30
|
+
const safe = appName
|
|
31
|
+
.split(/[-_\s]+/)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
34
|
+
.join('');
|
|
35
|
+
|
|
36
|
+
if (!safe) {
|
|
37
|
+
return 'appRoutes';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return `${safe.charAt(0).toLowerCase()}${safe.slice(1)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readDefaultExport(filePath) {
|
|
44
|
+
const moduleUrl = `${pathToFileURL(filePath).href}?t=${Date.now()}`;
|
|
45
|
+
const loaded = await import(moduleUrl);
|
|
46
|
+
return loaded?.default;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function detectSettingsMode(projectRoot) {
|
|
50
|
+
const single = path.join(projectRoot, 'settings.js');
|
|
51
|
+
const split = path.join(projectRoot, 'settings', 'apps.js');
|
|
52
|
+
|
|
53
|
+
if (await exists(single)) {
|
|
54
|
+
return { mode: 'single', file: single };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (await exists(split)) {
|
|
58
|
+
return { mode: 'split', file: split };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new Error(`Not an AegisNode project root: missing ${single} (or legacy ${split})`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readAppsConfig(settingsMode) {
|
|
65
|
+
const normalizeApp = (entry) => {
|
|
66
|
+
if (!entry || typeof entry !== 'object' || typeof entry.name !== 'string') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ensureValidName(entry.name, 'app');
|
|
71
|
+
return {
|
|
72
|
+
name: entry.name,
|
|
73
|
+
mount: normalizeMountPath(entry.mount || `/${entry.name}`),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (settingsMode.mode === 'single') {
|
|
78
|
+
const settings = await readDefaultExport(settingsMode.file);
|
|
79
|
+
const apps = settings?.apps;
|
|
80
|
+
|
|
81
|
+
if (!Array.isArray(apps)) {
|
|
82
|
+
throw new Error(`settings.js must export { apps: [] }. File: ${settingsMode.file}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return apps.map(normalizeApp).filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const apps = await readDefaultExport(settingsMode.file);
|
|
89
|
+
if (!Array.isArray(apps)) {
|
|
90
|
+
throw new Error(`settings/apps.js must export an array. File: ${settingsMode.file}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return apps.map(normalizeApp).filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function updateSingleSettingsApps(settingsFile, apps) {
|
|
97
|
+
const current = await fs.readFile(settingsFile, 'utf8');
|
|
98
|
+
|
|
99
|
+
if (!current.includes(APPS_START) || !current.includes(APPS_END)) {
|
|
100
|
+
throw new Error(`settings.js is missing ${APPS_START}/${APPS_END} markers: ${settingsFile}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const replacement = `${APPS_START}\n${renderAppEntries(apps)}\n ${APPS_END}`;
|
|
104
|
+
const updated = current.replace(/\/\/ AEGIS_APPS_START[\s\S]*?\/\/ AEGIS_APPS_END/m, replacement);
|
|
105
|
+
|
|
106
|
+
await writeFile(settingsFile, updated);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function updateAppRegistry(projectRoot, apps, settingsMode) {
|
|
110
|
+
if (settingsMode.mode === 'single') {
|
|
111
|
+
await updateSingleSettingsApps(settingsMode.file, apps);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await writeFile(path.join(projectRoot, 'settings', 'apps.js'), renderSettingsApps(apps));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function updateProjectRoutesFile(projectRoot, appName, mountPath) {
|
|
119
|
+
const routesFile = path.join(projectRoot, 'routes.js');
|
|
120
|
+
if (!(await exists(routesFile))) {
|
|
121
|
+
// Keep backward compatibility for legacy projects that still use routes/index.js.
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const importName = toImportName(appName);
|
|
126
|
+
const importLine = `import ${importName} from './apps/${appName}/routes.js';`;
|
|
127
|
+
const routeLine = ` route.use(${JSON.stringify(mountPath)}, ${importName});`;
|
|
128
|
+
|
|
129
|
+
let content = await fs.readFile(routesFile, 'utf8');
|
|
130
|
+
|
|
131
|
+
if (!content.includes(importLine)) {
|
|
132
|
+
if (content.includes('// AEGIS_APP_IMPORTS_START') && content.includes('// AEGIS_APP_IMPORTS_END')) {
|
|
133
|
+
content = content.replace('// AEGIS_APP_IMPORTS_END', `${importLine}\n// AEGIS_APP_IMPORTS_END`);
|
|
134
|
+
} else {
|
|
135
|
+
content = `${importLine}\n${content}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!content.includes(routeLine)) {
|
|
140
|
+
if (content.includes('// AEGIS_PROJECT_APP_ROUTES_START') && content.includes('// AEGIS_PROJECT_APP_ROUTES_END')) {
|
|
141
|
+
content = content.replace(' // AEGIS_PROJECT_APP_ROUTES_END', `${routeLine}\n // AEGIS_PROJECT_APP_ROUTES_END`);
|
|
142
|
+
} else {
|
|
143
|
+
const match = content.match(/register\s*\([^)]*\)\s*{[\s\S]*?\n\s*}/m);
|
|
144
|
+
if (match) {
|
|
145
|
+
content = content.replace(match[0], `${match[0]}\n${routeLine}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await writeFile(routesFile, content);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function createScaffold(projectRoot, appName) {
|
|
154
|
+
const appRoot = path.join(projectRoot, 'apps', appName);
|
|
155
|
+
|
|
156
|
+
await ensureDir(appRoot);
|
|
157
|
+
await ensureDir(path.join(appRoot, 'tests'));
|
|
158
|
+
|
|
159
|
+
await writeFile(path.join(appRoot, 'views.js'), renderAppViewsFile(appName));
|
|
160
|
+
await writeFile(path.join(appRoot, 'models.js'), renderAppModelsFile(appName));
|
|
161
|
+
await writeFile(path.join(appRoot, 'validators.js'), renderAppValidatorsFile(appName));
|
|
162
|
+
await writeFile(path.join(appRoot, 'services.js'), renderAppServicesFile(appName));
|
|
163
|
+
await writeFile(path.join(appRoot, 'subscribers.js'), renderAppSubscribersFile(appName));
|
|
164
|
+
await writeFile(path.join(appRoot, 'routes.js'), renderAppRoutes(appName));
|
|
165
|
+
await writeFile(path.join(appRoot, 'tests', 'models.test.js'), renderAppModelTest(appName));
|
|
166
|
+
await writeFile(path.join(appRoot, 'tests', 'validators.test.js'), renderAppValidatorTest(appName));
|
|
167
|
+
await writeFile(path.join(appRoot, 'tests', 'services.test.js'), renderAppServiceTest(appName));
|
|
168
|
+
await writeFile(path.join(appRoot, 'tests', 'routes.test.js'), renderAppRoutesTest(appName));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function createApp({ appName, projectRoot, mount }) {
|
|
172
|
+
ensureValidName(appName, 'app');
|
|
173
|
+
|
|
174
|
+
const resolvedRoot = await resolveProjectRoot(projectRoot);
|
|
175
|
+
|
|
176
|
+
const settingsMode = await detectSettingsMode(resolvedRoot);
|
|
177
|
+
const normalizedMount = normalizeMountPath(mount || `/${appName}`);
|
|
178
|
+
const existingApps = await readAppsConfig(settingsMode);
|
|
179
|
+
|
|
180
|
+
if (existingApps.some((entry) => entry.name === appName)) {
|
|
181
|
+
throw new Error(`App \"${appName}\" already exists in project settings`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const updatedApps = [...existingApps, { name: appName, mount: normalizedMount }];
|
|
185
|
+
|
|
186
|
+
await createScaffold(resolvedRoot, appName);
|
|
187
|
+
await updateAppRegistry(resolvedRoot, updatedApps, settingsMode);
|
|
188
|
+
await updateProjectRoutesFile(resolvedRoot, appName, normalizedMount);
|
|
189
|
+
|
|
190
|
+
console.log(`App \"${appName}\" created at ${path.join(resolvedRoot, 'apps', appName)}`);
|
|
191
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { loadProjectConfig } from '../../runtime/config.js';
|
|
4
|
+
import { resolveProjectRoot } from '../utils/project.js';
|
|
5
|
+
|
|
6
|
+
function createCollector() {
|
|
7
|
+
const entries = [];
|
|
8
|
+
return {
|
|
9
|
+
entries,
|
|
10
|
+
ok: (message) => entries.push({ level: 'OK', message }),
|
|
11
|
+
warn: (message) => entries.push({ level: 'WARN', message }),
|
|
12
|
+
error: (message) => entries.push({ level: 'ERROR', message }),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasStrongSecret(value) {
|
|
17
|
+
return typeof value === 'string' && value.trim().length >= 16;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function fileExists(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
await fs.access(filePath);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function runAppChecks(rootDir, config, collector) {
|
|
30
|
+
const apps = Array.isArray(config.apps) ? config.apps : [];
|
|
31
|
+
|
|
32
|
+
if (apps.length === 0) {
|
|
33
|
+
collector.warn('No apps declared in settings.apps.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
collector.ok(`Declared apps: ${apps.map((app) => app.name).join(', ')}`);
|
|
38
|
+
|
|
39
|
+
for (const app of apps) {
|
|
40
|
+
const appName = app?.name;
|
|
41
|
+
const mount = app?.mount;
|
|
42
|
+
if (typeof appName !== 'string' || appName.trim().length === 0) {
|
|
43
|
+
collector.error(`Invalid app entry: ${JSON.stringify(app)}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof mount !== 'string' || !mount.startsWith('/')) {
|
|
48
|
+
collector.error(`App "${appName}" has invalid mount "${String(mount)}" (must start with /).`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const appRoot = path.join(rootDir, 'apps', appName);
|
|
52
|
+
const appRootExists = await fileExists(appRoot);
|
|
53
|
+
if (!appRootExists) {
|
|
54
|
+
collector.error(`App directory missing: apps/${appName}`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const requiredFiles = ['routes.js', 'views.js', 'services.js', 'models.js', 'validators.js'];
|
|
59
|
+
for (const fileName of requiredFiles) {
|
|
60
|
+
const target = path.join(appRoot, fileName);
|
|
61
|
+
if (!(await fileExists(target))) {
|
|
62
|
+
collector.warn(`App "${appName}" missing ${fileName}.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const subscribersFile = path.join(appRoot, 'subscribers.js');
|
|
67
|
+
if (!(await fileExists(subscribersFile))) {
|
|
68
|
+
collector.warn(`App "${appName}" missing subscribers.js.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runSecurityChecks(config, collector) {
|
|
74
|
+
const env = String(config.env || process.env.NODE_ENV || 'development');
|
|
75
|
+
const security = config.security || {};
|
|
76
|
+
|
|
77
|
+
if (!hasStrongSecret(security.appSecret)) {
|
|
78
|
+
if (env === 'production') {
|
|
79
|
+
collector.error('security.appSecret is missing/weak for production (min length: 16).');
|
|
80
|
+
} else {
|
|
81
|
+
collector.warn('security.appSecret is missing/weak (recommended min length: 16).');
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
collector.ok('security.appSecret is set.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (security?.headers?.enabled === false) {
|
|
88
|
+
collector.warn('security.headers.enabled is false.');
|
|
89
|
+
}
|
|
90
|
+
if (security?.csrf?.enabled === false) {
|
|
91
|
+
collector.warn('security.csrf.enabled is false.');
|
|
92
|
+
}
|
|
93
|
+
if (security?.ddos?.enabled === false) {
|
|
94
|
+
collector.warn('security.ddos.enabled is false.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function runAuthChecks(config, collector) {
|
|
99
|
+
const auth = config.auth || {};
|
|
100
|
+
if (auth.enabled !== true) {
|
|
101
|
+
collector.ok('Auth subsystem disabled.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
collector.ok(`Auth subsystem enabled (${auth.provider || 'jwt'}).`);
|
|
106
|
+
|
|
107
|
+
if (auth.provider === 'jwt') {
|
|
108
|
+
const jwtSecret = auth?.jwt?.secret;
|
|
109
|
+
const appSecret = config?.security?.appSecret;
|
|
110
|
+
if (!hasStrongSecret(jwtSecret) && !hasStrongSecret(appSecret)) {
|
|
111
|
+
collector.error('JWT enabled but neither auth.jwt.secret nor security.appSecret is strong.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (auth.provider === 'oauth2' && config.env === 'production') {
|
|
116
|
+
if (auth?.oauth2?.server?.allowHttp === true) {
|
|
117
|
+
collector.error('OAuth2 server allowHttp=true in production.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function runApiChecks(config, collector) {
|
|
123
|
+
const apiApps = Array.isArray(config?.api?.apps) ? config.api.apps : [];
|
|
124
|
+
const appNames = new Set((Array.isArray(config.apps) ? config.apps : []).map((app) => app.name));
|
|
125
|
+
|
|
126
|
+
for (const apiAppName of apiApps) {
|
|
127
|
+
if (!appNames.has(apiAppName)) {
|
|
128
|
+
collector.warn(`api.apps references unknown app "${apiAppName}".`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function runTemplateChecks(rootDir, config, collector) {
|
|
134
|
+
const templates = config.templates || {};
|
|
135
|
+
if (templates.enabled === false) {
|
|
136
|
+
collector.ok('Templates disabled.');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const templatesDir = typeof templates.dir === 'string' && templates.dir.trim().length > 0
|
|
141
|
+
? templates.dir.trim()
|
|
142
|
+
: 'templates';
|
|
143
|
+
const templatesRoot = path.isAbsolute(templatesDir)
|
|
144
|
+
? templatesDir
|
|
145
|
+
: path.join(rootDir, templatesDir);
|
|
146
|
+
|
|
147
|
+
if (!(await fileExists(templatesRoot))) {
|
|
148
|
+
collector.warn(`Template directory does not exist: ${templatesRoot}`);
|
|
149
|
+
} else {
|
|
150
|
+
collector.ok(`Template directory exists: ${templatesRoot}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function printSummary(entries, output = console) {
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
output.log(`[${entry.level}] ${entry.message}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const errors = entries.filter((item) => item.level === 'ERROR').length;
|
|
160
|
+
const warnings = entries.filter((item) => item.level === 'WARN').length;
|
|
161
|
+
const oks = entries.filter((item) => item.level === 'OK').length;
|
|
162
|
+
|
|
163
|
+
output.log('');
|
|
164
|
+
output.log(`Doctor summary: ${errors} error(s), ${warnings} warning(s), ${oks} ok.`);
|
|
165
|
+
return { errors, warnings, oks };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function runDoctor({
|
|
169
|
+
projectRoot,
|
|
170
|
+
failOnError = true,
|
|
171
|
+
output = console,
|
|
172
|
+
} = {}) {
|
|
173
|
+
const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
|
|
174
|
+
const collector = createCollector();
|
|
175
|
+
|
|
176
|
+
collector.ok(`Project root: ${resolvedRoot}`);
|
|
177
|
+
|
|
178
|
+
const config = await loadProjectConfig(resolvedRoot);
|
|
179
|
+
collector.ok(`Environment: ${config.env || 'development'}`);
|
|
180
|
+
|
|
181
|
+
await runAppChecks(resolvedRoot, config, collector);
|
|
182
|
+
runSecurityChecks(config, collector);
|
|
183
|
+
runAuthChecks(config, collector);
|
|
184
|
+
runApiChecks(config, collector);
|
|
185
|
+
await runTemplateChecks(resolvedRoot, config, collector);
|
|
186
|
+
|
|
187
|
+
const summary = printSummary(collector.entries, output);
|
|
188
|
+
|
|
189
|
+
if (failOnError && summary.errors > 0) {
|
|
190
|
+
throw new Error('Doctor failed with configuration errors.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
rootDir: resolvedRoot,
|
|
195
|
+
config,
|
|
196
|
+
entries: collector.entries,
|
|
197
|
+
summary,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ensureValidName, exists, writeFile } from '../utils/fs.js';
|
|
4
|
+
import { toPascalCase } from '../utils/scaffolds.js';
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_TYPES = new Set(['view', 'controller', 'model', 'validator', 'dto', 'service', 'subscriber', 'route']);
|
|
7
|
+
|
|
8
|
+
function normalizeType(type) {
|
|
9
|
+
if (type === 'controller') {
|
|
10
|
+
return 'view';
|
|
11
|
+
}
|
|
12
|
+
if (type === 'dto') {
|
|
13
|
+
return 'validator';
|
|
14
|
+
}
|
|
15
|
+
return type;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assertType(type) {
|
|
19
|
+
if (!SUPPORTED_TYPES.has(type)) {
|
|
20
|
+
throw new Error(`Unsupported generate type \"${type}\". Supported: ${Array.from(SUPPORTED_TYPES).join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function assertProjectRoot(projectRoot) {
|
|
25
|
+
const hasSingleSettings = await exists(path.join(projectRoot, 'settings.js'));
|
|
26
|
+
const hasLegacySettings = (await exists(path.join(projectRoot, 'settings', 'index.js')))
|
|
27
|
+
|| (await exists(path.join(projectRoot, 'settings', 'apps.js')));
|
|
28
|
+
|
|
29
|
+
if (!hasSingleSettings && !hasLegacySettings) {
|
|
30
|
+
throw new Error('Not an AegisNode project root: missing settings.js');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function assertAppRoot(projectRoot, appName) {
|
|
35
|
+
const appRoot = path.join(projectRoot, 'apps', appName);
|
|
36
|
+
if (!(await exists(appRoot))) {
|
|
37
|
+
throw new Error(`App \"${appName}\" does not exist. Create it first with: aegisnode createapp ${appName}`);
|
|
38
|
+
}
|
|
39
|
+
return appRoot;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderView(name, appName) {
|
|
43
|
+
const className = `${toPascalCase(name)}View`;
|
|
44
|
+
return `class ${className} {
|
|
45
|
+
static index(req, res) {
|
|
46
|
+
res.json({
|
|
47
|
+
app: '${appName}',
|
|
48
|
+
view: '${name}',
|
|
49
|
+
message: 'Generated by AegisNode',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default ${className};
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderModel(name) {
|
|
59
|
+
const className = `${toPascalCase(name)}Model`;
|
|
60
|
+
return `class ${className} {
|
|
61
|
+
constructor({ dbClient }) {
|
|
62
|
+
this.dbClient = dbClient;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default ${className};
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderService(name) {
|
|
71
|
+
const className = `${toPascalCase(name)}Service`;
|
|
72
|
+
return `class ${className} {
|
|
73
|
+
constructor({ models }) {
|
|
74
|
+
this.models = models;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default ${className};
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderValidator(name) {
|
|
83
|
+
const className = `${toPascalCase(name)}Validator`;
|
|
84
|
+
return `class ${className} {
|
|
85
|
+
normalize(payload) {
|
|
86
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
87
|
+
const error = new Error('Payload must be an object.');
|
|
88
|
+
error.statusCode = 400;
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
return payload;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default ${className};
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderSubscriber(name, appName) {
|
|
100
|
+
const fnName = `register${toPascalCase(name)}Subscriber`;
|
|
101
|
+
return `export default function ${fnName}({ events, logger }) {
|
|
102
|
+
events.subscribe('app.booted', ({ appName: mountedApp }) => {
|
|
103
|
+
if (mountedApp !== '${appName}') {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.debug('[subscriber:${name}] ${appName} booted');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function getAppRoutesFile(appRoot) {
|
|
114
|
+
const primary = path.join(appRoot, 'routes.js');
|
|
115
|
+
if (await exists(primary)) {
|
|
116
|
+
return primary;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const legacy = path.join(appRoot, 'routes', 'index.js');
|
|
120
|
+
if (await exists(legacy)) {
|
|
121
|
+
return legacy;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
throw new Error(`Missing app routes file in ${appRoot}. Expected routes.js or routes/index.js`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function appendRouteToApp({ appRoot, routeName }) {
|
|
128
|
+
const routesFile = await getAppRoutesFile(appRoot);
|
|
129
|
+
const usesFlatRoutesFile = path.basename(routesFile) === 'routes.js';
|
|
130
|
+
const flatViewPath = path.join(appRoot, `${routeName}.view.js`);
|
|
131
|
+
const nestedViewPath = path.join(appRoot, 'views', `${routeName}.view.js`);
|
|
132
|
+
|
|
133
|
+
let importPath = null;
|
|
134
|
+
if (await exists(flatViewPath)) {
|
|
135
|
+
importPath = usesFlatRoutesFile
|
|
136
|
+
? `./${routeName}.view.js`
|
|
137
|
+
: `../${routeName}.view.js`;
|
|
138
|
+
} else if (await exists(nestedViewPath)) {
|
|
139
|
+
importPath = usesFlatRoutesFile
|
|
140
|
+
? `./views/${routeName}.view.js`
|
|
141
|
+
: `../views/${routeName}.view.js`;
|
|
142
|
+
} else {
|
|
143
|
+
throw new Error(`Missing view for route generation: ${flatViewPath} (or ${nestedViewPath}). Create it first with generate view ${routeName}.`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const viewClass = `${toPascalCase(routeName)}View`;
|
|
147
|
+
const importLine = `import ${viewClass} from '${importPath}';`;
|
|
148
|
+
const routeLine = ` route.get('/${routeName}', ${viewClass}.index);`;
|
|
149
|
+
|
|
150
|
+
let content = await fs.readFile(routesFile, 'utf8');
|
|
151
|
+
|
|
152
|
+
if (!content.includes(importLine)) {
|
|
153
|
+
const lines = content.split('\n');
|
|
154
|
+
let lastImportIndex = -1;
|
|
155
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
156
|
+
if (lines[i].startsWith('import ')) {
|
|
157
|
+
lastImportIndex = i;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (lastImportIndex >= 0) {
|
|
162
|
+
lines.splice(lastImportIndex + 1, 0, importLine);
|
|
163
|
+
} else {
|
|
164
|
+
lines.unshift(importLine);
|
|
165
|
+
}
|
|
166
|
+
content = lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!content.includes(routeLine)) {
|
|
170
|
+
const markerEnd = ' // AEGIS_APP_EXTRA_ROUTES_END';
|
|
171
|
+
if (content.includes(markerEnd)) {
|
|
172
|
+
content = content.replace(markerEnd, `${routeLine}\n${markerEnd}`);
|
|
173
|
+
} else {
|
|
174
|
+
const lines = content.split('\n');
|
|
175
|
+
let closeIndex = -1;
|
|
176
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
177
|
+
if (lines[i].trim() === '},' || lines[i].trim() === '})' || lines[i].trim() === '};') {
|
|
178
|
+
closeIndex = i;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (closeIndex < 0) {
|
|
184
|
+
throw new Error(`Cannot auto-wire route in ${routesFile}. Could not find object block end.`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
lines.splice(closeIndex, 0, routeLine);
|
|
188
|
+
content = lines.join('\n');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
await writeFile(routesFile, content);
|
|
193
|
+
return routesFile;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveTarget(appRoot, type, name) {
|
|
197
|
+
switch (type) {
|
|
198
|
+
case 'view':
|
|
199
|
+
return path.join(appRoot, `${name}.view.js`);
|
|
200
|
+
case 'model':
|
|
201
|
+
return path.join(appRoot, `${name}.model.js`);
|
|
202
|
+
case 'service':
|
|
203
|
+
return path.join(appRoot, `${name}.service.js`);
|
|
204
|
+
case 'validator':
|
|
205
|
+
return path.join(appRoot, `${name}.validator.js`);
|
|
206
|
+
case 'subscriber':
|
|
207
|
+
return path.join(appRoot, `${name}.subscriber.js`);
|
|
208
|
+
default:
|
|
209
|
+
throw new Error(`Unsupported type: ${type}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderContent(type, name, appName) {
|
|
214
|
+
switch (type) {
|
|
215
|
+
case 'view':
|
|
216
|
+
return renderView(name, appName);
|
|
217
|
+
case 'model':
|
|
218
|
+
return renderModel(name);
|
|
219
|
+
case 'service':
|
|
220
|
+
return renderService(name);
|
|
221
|
+
case 'validator':
|
|
222
|
+
return renderValidator(name);
|
|
223
|
+
case 'subscriber':
|
|
224
|
+
return renderSubscriber(name, appName);
|
|
225
|
+
default:
|
|
226
|
+
throw new Error(`Unsupported type: ${type}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function generateArtifact({ type, name, appName, projectRoot }) {
|
|
231
|
+
if (!type) {
|
|
232
|
+
throw new Error('Missing generate type. Usage: aegisnode generate <type> <name> --app <app-name>');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!name) {
|
|
236
|
+
throw new Error('Missing artifact name. Usage: aegisnode generate <type> <name> --app <app-name>');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!appName) {
|
|
240
|
+
throw new Error('Missing app name. Use --app <app-name>');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
assertType(type);
|
|
244
|
+
const normalizedType = normalizeType(type);
|
|
245
|
+
ensureValidName(name, normalizedType);
|
|
246
|
+
ensureValidName(appName, 'app');
|
|
247
|
+
|
|
248
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
249
|
+
await assertProjectRoot(resolvedRoot);
|
|
250
|
+
const appRoot = await assertAppRoot(resolvedRoot, appName);
|
|
251
|
+
|
|
252
|
+
if (normalizedType === 'route') {
|
|
253
|
+
const routesFile = await appendRouteToApp({ appRoot, routeName: name });
|
|
254
|
+
console.log(`Generated route /${name} in ${routesFile}`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const targetFile = resolveTarget(appRoot, normalizedType, name);
|
|
259
|
+
if (await exists(targetFile)) {
|
|
260
|
+
throw new Error(`File already exists: ${targetFile}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await writeFile(targetFile, renderContent(normalizedType, name, appName));
|
|
264
|
+
|
|
265
|
+
console.log(`Generated ${normalizedType}: ${targetFile}`);
|
|
266
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { runProject } from '../../index.js';
|
|
2
|
+
import { resolveProjectRoot } from '../utils/project.js';
|
|
3
|
+
|
|
4
|
+
export async function runServer({ projectRoot, port }) {
|
|
5
|
+
const resolvedRoot = await resolveProjectRoot(projectRoot);
|
|
6
|
+
const overrides = {};
|
|
7
|
+
|
|
8
|
+
if (Number.isFinite(port) && port >= 0) {
|
|
9
|
+
overrides.port = Number(port);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return runProject({
|
|
13
|
+
rootDir: resolvedRoot,
|
|
14
|
+
overrides,
|
|
15
|
+
startupSource: 'runserver',
|
|
16
|
+
});
|
|
17
|
+
}
|