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,596 @@
|
|
|
1
|
+
export function toPascalCase(value) {
|
|
2
|
+
return value
|
|
3
|
+
.split(/[-_\s]+/)
|
|
4
|
+
.filter(Boolean)
|
|
5
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
6
|
+
.join('');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function renderAppEntries(apps, indent = ' ') {
|
|
10
|
+
return apps
|
|
11
|
+
.map((app) => `${indent}{ name: ${JSON.stringify(app.name)}, mount: ${JSON.stringify(app.mount)} },`)
|
|
12
|
+
.join('\n');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderProjectPackageJson(projectName) {
|
|
16
|
+
return `${JSON.stringify(
|
|
17
|
+
{
|
|
18
|
+
name: projectName,
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
private: true,
|
|
21
|
+
type: 'module',
|
|
22
|
+
scripts: {
|
|
23
|
+
dev: 'aegisnode runserver',
|
|
24
|
+
start: 'node loader.cjs',
|
|
25
|
+
test: 'node --test',
|
|
26
|
+
},
|
|
27
|
+
dependencies: {
|
|
28
|
+
aegisnode: '^0.1.0',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
null,
|
|
32
|
+
2,
|
|
33
|
+
)}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderProjectAppJs() {
|
|
37
|
+
return `import path from 'path';
|
|
38
|
+
import { fileURLToPath } from 'url';
|
|
39
|
+
import { runProject } from 'aegisnode';
|
|
40
|
+
|
|
41
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
42
|
+
const __dirname = path.dirname(__filename);
|
|
43
|
+
|
|
44
|
+
runProject({ rootDir: __dirname });
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function renderProjectLoaderCjs() {
|
|
49
|
+
return `import('./app.js').catch((error) => {
|
|
50
|
+
console.error(error?.stack || error?.message || String(error));
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
});
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function renderProjectSettings(projectName, apps) {
|
|
57
|
+
return `export default {
|
|
58
|
+
appName: '${projectName}',
|
|
59
|
+
env: process.env.NODE_ENV || 'development',
|
|
60
|
+
host: process.env.HOST || '0.0.0.0',
|
|
61
|
+
port: process.env.PORT ? Number(process.env.PORT) : 3000,
|
|
62
|
+
trustProxy: false,
|
|
63
|
+
security: {
|
|
64
|
+
// Loaded from .env by default. Replace or rotate APP_SECRET in production.
|
|
65
|
+
appSecret: process.env.APP_SECRET || '',
|
|
66
|
+
},
|
|
67
|
+
logging: {
|
|
68
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
69
|
+
},
|
|
70
|
+
database: {
|
|
71
|
+
enabled: false,
|
|
72
|
+
dialect: 'pg',
|
|
73
|
+
config: {},
|
|
74
|
+
options: {},
|
|
75
|
+
},
|
|
76
|
+
cache: {
|
|
77
|
+
enabled: true,
|
|
78
|
+
driver: 'memory',
|
|
79
|
+
options: {},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Optional sections you can add manually when needed:
|
|
83
|
+
// maintenance, https, templates, i18n, helpers, staticDir, websocket, uploads, auth, api, mail, swagger,
|
|
84
|
+
// architecture, loaders, environments, security.headers/ddos/csrf
|
|
85
|
+
|
|
86
|
+
apps: [
|
|
87
|
+
// AEGIS_APPS_START
|
|
88
|
+
${renderAppEntries(apps, ' ')}
|
|
89
|
+
// AEGIS_APPS_END
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
export function renderSettingsApps(apps) {
|
|
95
|
+
return `const apps = [
|
|
96
|
+
// AEGIS_APPS_START
|
|
97
|
+
${renderAppEntries(apps, ' ')}
|
|
98
|
+
// AEGIS_APPS_END
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
export default apps;
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function renderProjectRoutes() {
|
|
106
|
+
return `// AEGIS_APP_IMPORTS_START
|
|
107
|
+
// AEGIS_APP_IMPORTS_END
|
|
108
|
+
|
|
109
|
+
export default {
|
|
110
|
+
register(route) {
|
|
111
|
+
route.get('/health', (req, res) => {
|
|
112
|
+
res.json({ status: 'ok' });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// AEGIS_PROJECT_APP_ROUTES_START
|
|
116
|
+
// route.use('/users', users);
|
|
117
|
+
// AEGIS_PROJECT_APP_ROUTES_END
|
|
118
|
+
|
|
119
|
+
// Define your own homepage if you want to override the default confirmation page:
|
|
120
|
+
// route.get('/', (req, res) => {
|
|
121
|
+
// return res.render('home', { title: 'Home' });
|
|
122
|
+
// });
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function renderProjectGitIgnore() {
|
|
129
|
+
return `node_modules
|
|
130
|
+
.env
|
|
131
|
+
.aegis/
|
|
132
|
+
.DS_Store
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function renderEnvExample() {
|
|
137
|
+
return `APP_SECRET=replace-with-strong-secret
|
|
138
|
+
PORT=3000
|
|
139
|
+
LOG_LEVEL=info
|
|
140
|
+
NODE_ENV=development
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function renderProjectEnv(appSecret) {
|
|
145
|
+
return `APP_SECRET=${appSecret}
|
|
146
|
+
PORT=3000
|
|
147
|
+
LOG_LEVEL=info
|
|
148
|
+
NODE_ENV=development
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function renderView(appName) {
|
|
153
|
+
const className = `${toPascalCase(appName)}View`;
|
|
154
|
+
return `class ${className} {
|
|
155
|
+
static index(_context, req, res, next) {
|
|
156
|
+
try {
|
|
157
|
+
res.json({
|
|
158
|
+
app: '${appName}',
|
|
159
|
+
message: 'Hello from ${appName} view',
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (typeof next === 'function') {
|
|
163
|
+
next(error);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default ${className};
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function renderController(appName) {
|
|
176
|
+
return renderView(appName);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function renderAppViewsFile(appName) {
|
|
180
|
+
const className = `${toPascalCase(appName)}View`;
|
|
181
|
+
return `class ${className} {
|
|
182
|
+
static home(_context, req, res, next) {
|
|
183
|
+
try {
|
|
184
|
+
res.json({
|
|
185
|
+
app: ${JSON.stringify(appName)},
|
|
186
|
+
message: 'Hello from ${appName} view',
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
next(error);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static async index({ service }, req, res, next) {
|
|
194
|
+
try {
|
|
195
|
+
const data = await service.list();
|
|
196
|
+
res.json({ data });
|
|
197
|
+
} catch (error) {
|
|
198
|
+
next(error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static async create({ service, validator }, req, res, next) {
|
|
203
|
+
try {
|
|
204
|
+
const payload = validator.create(req.body || {});
|
|
205
|
+
const created = await service.create(payload);
|
|
206
|
+
res.status(201).json({ data: created });
|
|
207
|
+
} catch (error) {
|
|
208
|
+
next(error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static async read({ service, validator }, req, res, next) {
|
|
213
|
+
try {
|
|
214
|
+
const id = validator.id(req.params.id);
|
|
215
|
+
const item = await service.getById(id);
|
|
216
|
+
if (!item) {
|
|
217
|
+
res.status(404).json({ error: 'Not Found' });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
res.json({ data: item });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
next(error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
static async update({ service, validator }, req, res, next) {
|
|
227
|
+
try {
|
|
228
|
+
const id = validator.id(req.params.id);
|
|
229
|
+
const payload = validator.update(req.body || {});
|
|
230
|
+
const updated = await service.update(id, payload);
|
|
231
|
+
if (!updated) {
|
|
232
|
+
res.status(404).json({ error: 'Not Found' });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
res.json({ data: updated });
|
|
236
|
+
} catch (error) {
|
|
237
|
+
next(error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
static async remove({ service, validator }, req, res, next) {
|
|
242
|
+
try {
|
|
243
|
+
const id = validator.id(req.params.id);
|
|
244
|
+
const removed = await service.remove(id);
|
|
245
|
+
if (!removed) {
|
|
246
|
+
res.status(404).json({ error: 'Not Found' });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
res.status(204).end();
|
|
250
|
+
} catch (error) {
|
|
251
|
+
next(error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default ${className};
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function renderModel(appName) {
|
|
261
|
+
const className = `${toPascalCase(appName)}Model`;
|
|
262
|
+
return `class ${className} {
|
|
263
|
+
constructor({ dbClient }) {
|
|
264
|
+
this.dbClient = dbClient;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export default ${className};
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function renderService(appName) {
|
|
273
|
+
const className = `${toPascalCase(appName)}Service`;
|
|
274
|
+
return `class ${className} {
|
|
275
|
+
constructor({ models }) {
|
|
276
|
+
this.models = models;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export default ${className};
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function renderSubscriber(appName) {
|
|
285
|
+
return `export default function register${toPascalCase(appName)}Subscribers({ events, logger }) {
|
|
286
|
+
events.subscribe('app.booted', ({ appName }) => {
|
|
287
|
+
logger.debug('[subscriber:${appName}] received app.booted for %s', appName);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function renderAppModelsFile(appName) {
|
|
294
|
+
const className = `${toPascalCase(appName)}Model`;
|
|
295
|
+
return `const records = [];
|
|
296
|
+
let nextId = 1;
|
|
297
|
+
|
|
298
|
+
function sanitizePayload(payload) {
|
|
299
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const cleaned = {};
|
|
304
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
305
|
+
if (key === 'id' || typeof value === 'undefined') {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
cleaned[key] = value;
|
|
309
|
+
}
|
|
310
|
+
return cleaned;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
class ${className} {
|
|
314
|
+
constructor({ dbClient }) {
|
|
315
|
+
this.dbClient = dbClient;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async list() {
|
|
319
|
+
return records.map((entry) => ({ ...entry }));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async getById(id) {
|
|
323
|
+
const record = records.find((entry) => entry.id === String(id));
|
|
324
|
+
return record ? { ...record } : null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async create(payload) {
|
|
328
|
+
const entry = {
|
|
329
|
+
id: String(nextId++),
|
|
330
|
+
...sanitizePayload(payload),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
records.push(entry);
|
|
334
|
+
return { ...entry };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async update(id, payload) {
|
|
338
|
+
const targetId = String(id);
|
|
339
|
+
const index = records.findIndex((entry) => entry.id === targetId);
|
|
340
|
+
if (index < 0) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
records[index] = {
|
|
345
|
+
...records[index],
|
|
346
|
+
...sanitizePayload(payload),
|
|
347
|
+
id: targetId,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
return { ...records[index] };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async remove(id) {
|
|
354
|
+
const targetId = String(id);
|
|
355
|
+
const index = records.findIndex((entry) => entry.id === targetId);
|
|
356
|
+
if (index < 0) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
records.splice(index, 1);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export default {
|
|
366
|
+
${JSON.stringify(appName)}: ${className},
|
|
367
|
+
};
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function renderAppServicesFile(appName) {
|
|
372
|
+
const className = `${toPascalCase(appName)}Service`;
|
|
373
|
+
return `class ${className} {
|
|
374
|
+
constructor({ models }) {
|
|
375
|
+
this.model = models.get(${JSON.stringify(appName)});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async list() {
|
|
379
|
+
return this.model.list();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async getById(id) {
|
|
383
|
+
return this.model.getById(id);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async create(payload) {
|
|
387
|
+
return this.model.create(payload);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async update(id, payload) {
|
|
391
|
+
return this.model.update(id, payload);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async remove(id) {
|
|
395
|
+
return this.model.remove(id);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export default {
|
|
400
|
+
${JSON.stringify(appName)}: ${className},
|
|
401
|
+
};
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function renderAppSubscribersFile(appName) {
|
|
406
|
+
return `export default function register${toPascalCase(appName)}Subscribers({ events, logger }) {
|
|
407
|
+
events.subscribe('app.booted', ({ appName }) => {
|
|
408
|
+
logger.debug('[subscriber:${appName}] received app.booted for %s', appName);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function renderAppValidatorsFile(appName) {
|
|
415
|
+
const className = `${toPascalCase(appName)}Validator`;
|
|
416
|
+
return `class ${className} {
|
|
417
|
+
normalizePayload(payload) {
|
|
418
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
419
|
+
const error = new Error('Payload must be an object.');
|
|
420
|
+
error.statusCode = 400;
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const normalized = {};
|
|
425
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
426
|
+
if (key === 'id' || typeof value === 'undefined') {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
normalized[key] = value;
|
|
430
|
+
}
|
|
431
|
+
return normalized;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
id(value) {
|
|
435
|
+
const normalized = String(value || '').trim();
|
|
436
|
+
if (!normalized) {
|
|
437
|
+
const error = new Error('Invalid resource identifier.');
|
|
438
|
+
error.statusCode = 400;
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
return normalized;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
create(payload) {
|
|
445
|
+
return this.normalizePayload(payload);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
update(payload) {
|
|
449
|
+
return this.normalizePayload(payload);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export default {
|
|
454
|
+
${JSON.stringify(appName)}: ${className},
|
|
455
|
+
};
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function renderAppRoutes(appName) {
|
|
460
|
+
const viewClass = `${toPascalCase(appName)}View`;
|
|
461
|
+
return `import ${viewClass} from './views.js';
|
|
462
|
+
|
|
463
|
+
export default {
|
|
464
|
+
appName: ${JSON.stringify(appName)},
|
|
465
|
+
register(route) {
|
|
466
|
+
route.get('/home', ${viewClass}.home);
|
|
467
|
+
route.get('/', ${viewClass}.index);
|
|
468
|
+
route.post('/', ${viewClass}.create);
|
|
469
|
+
|
|
470
|
+
// AEGIS_APP_EXTRA_ROUTES_START
|
|
471
|
+
// AEGIS_APP_EXTRA_ROUTES_END
|
|
472
|
+
|
|
473
|
+
route.get('/:id', ${viewClass}.read);
|
|
474
|
+
route.put('/:id', ${viewClass}.update);
|
|
475
|
+
route.delete('/:id', ${viewClass}.remove);
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function renderAppModelTest(appName) {
|
|
482
|
+
return `import test from 'node:test';
|
|
483
|
+
import assert from 'node:assert/strict';
|
|
484
|
+
import models from '../models.js';
|
|
485
|
+
|
|
486
|
+
test('${appName} model exposes basic CRUD methods', async () => {
|
|
487
|
+
const Model = models[${JSON.stringify(appName)}];
|
|
488
|
+
assert.equal(typeof Model, 'function');
|
|
489
|
+
|
|
490
|
+
const model = new Model({ dbClient: null });
|
|
491
|
+
assert.equal(typeof model.list, 'function');
|
|
492
|
+
assert.equal(typeof model.getById, 'function');
|
|
493
|
+
assert.equal(typeof model.create, 'function');
|
|
494
|
+
assert.equal(typeof model.update, 'function');
|
|
495
|
+
assert.equal(typeof model.remove, 'function');
|
|
496
|
+
|
|
497
|
+
const created = await model.create({ name: 'Alice' });
|
|
498
|
+
assert.equal(created.name, 'Alice');
|
|
499
|
+
|
|
500
|
+
const found = await model.getById(created.id);
|
|
501
|
+
assert.equal(found?.name, 'Alice');
|
|
502
|
+
});
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function renderAppValidatorTest(appName) {
|
|
507
|
+
return `import test from 'node:test';
|
|
508
|
+
import assert from 'node:assert/strict';
|
|
509
|
+
import validators from '../validators.js';
|
|
510
|
+
|
|
511
|
+
test('${appName} validator validates id and payload', () => {
|
|
512
|
+
const Validator = validators[${JSON.stringify(appName)}];
|
|
513
|
+
assert.equal(typeof Validator, 'function');
|
|
514
|
+
|
|
515
|
+
const validator = new Validator();
|
|
516
|
+
const payload = validator.create({ name: 'Alice', id: 'ignore-me' });
|
|
517
|
+
assert.equal(payload.name, 'Alice');
|
|
518
|
+
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'id'), false);
|
|
519
|
+
|
|
520
|
+
assert.equal(validator.id('42'), '42');
|
|
521
|
+
assert.throws(() => validator.id(''), /Invalid resource identifier/);
|
|
522
|
+
});
|
|
523
|
+
`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function renderAppServiceTest(appName) {
|
|
527
|
+
return `import test from 'node:test';
|
|
528
|
+
import assert from 'node:assert/strict';
|
|
529
|
+
import services from '../services.js';
|
|
530
|
+
|
|
531
|
+
test('${appName} service delegates to model layer', async () => {
|
|
532
|
+
const Service = services[${JSON.stringify(appName)}];
|
|
533
|
+
assert.equal(typeof Service, 'function');
|
|
534
|
+
|
|
535
|
+
const fakeModel = {
|
|
536
|
+
async list() {
|
|
537
|
+
return [{ id: '1', name: 'Alice' }];
|
|
538
|
+
},
|
|
539
|
+
async getById(id) {
|
|
540
|
+
return { id: String(id), name: 'Alice' };
|
|
541
|
+
},
|
|
542
|
+
async create(payload) {
|
|
543
|
+
return { id: '2', ...payload };
|
|
544
|
+
},
|
|
545
|
+
async update(id, payload) {
|
|
546
|
+
return { id: String(id), ...payload };
|
|
547
|
+
},
|
|
548
|
+
async remove() {
|
|
549
|
+
return true;
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const service = new Service({
|
|
554
|
+
models: {
|
|
555
|
+
get(name) {
|
|
556
|
+
assert.equal(name, ${JSON.stringify(appName)});
|
|
557
|
+
return fakeModel;
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const listed = await service.list();
|
|
563
|
+
assert.equal(Array.isArray(listed), true);
|
|
564
|
+
assert.equal(listed[0]?.name, 'Alice');
|
|
565
|
+
});
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function renderAppRoutesTest(appName) {
|
|
570
|
+
return `import test from 'node:test';
|
|
571
|
+
import assert from 'node:assert/strict';
|
|
572
|
+
import routes from '../routes.js';
|
|
573
|
+
|
|
574
|
+
test('${appName} routes register expected CRUD endpoints', () => {
|
|
575
|
+
const calls = [];
|
|
576
|
+
const verbs = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all', 'use'];
|
|
577
|
+
const route = {};
|
|
578
|
+
|
|
579
|
+
for (const verb of verbs) {
|
|
580
|
+
route[verb] = (routePath) => {
|
|
581
|
+
calls.push(\`\${verb}:\${routePath}\`);
|
|
582
|
+
return route;
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
routes.register(route);
|
|
587
|
+
|
|
588
|
+
assert.equal(calls.includes('get:/home'), true);
|
|
589
|
+
assert.equal(calls.includes('get:/'), true);
|
|
590
|
+
assert.equal(calls.includes('post:/'), true);
|
|
591
|
+
assert.equal(calls.includes('get:/:id'), true);
|
|
592
|
+
assert.equal(calls.includes('put:/:id'), true);
|
|
593
|
+
assert.equal(calls.includes('delete:/:id'), true);
|
|
594
|
+
});
|
|
595
|
+
`;
|
|
596
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { runProject, createKernel, defineRoutes, defineProjectRoutes } from './runtime/kernel.js';
|
|
2
|
+
export { createContainer } from './runtime/container.js';
|
|
3
|
+
export { createEventBus } from './runtime/events.js';
|
|
4
|
+
export { createLogger } from './runtime/logger.js';
|
|
5
|
+
export { deepMerge, normalizeApps } from './runtime/config.js';
|
|
6
|
+
export { createAuthManager, normalizeAuthConfig, createAuthGuard } from './runtime/auth.js';
|
|
7
|
+
export { createMailManager, normalizeMailConfig } from './runtime/mail.js';
|
|
8
|
+
export {
|
|
9
|
+
money,
|
|
10
|
+
number,
|
|
11
|
+
dateTime,
|
|
12
|
+
timeElapsed,
|
|
13
|
+
timeDifference,
|
|
14
|
+
breakStr,
|
|
15
|
+
isObjectId,
|
|
16
|
+
toObjectId,
|
|
17
|
+
createHelpers,
|
|
18
|
+
loadJlive,
|
|
19
|
+
createRuntimeHelpers,
|
|
20
|
+
} from './runtime/helpers.js';
|