webspresso 0.0.73 → 0.0.75

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.
Files changed (65) hide show
  1. package/README.md +44 -4
  2. package/bin/commands/orm-map.js +139 -0
  3. package/bin/commands/skill.js +22 -8
  4. package/bin/commands/upgrade.js +146 -0
  5. package/bin/utils/orm-map-html.js +689 -0
  6. package/bin/utils/orm-map-load.js +85 -0
  7. package/bin/utils/orm-map-snapshot.js +179 -0
  8. package/bin/utils/resolve-webspresso-orm.js +23 -0
  9. package/bin/webspresso.js +4 -0
  10. package/core/auth/manager.js +14 -1
  11. package/core/kernel/app.js +96 -0
  12. package/core/kernel/base-repository.js +143 -0
  13. package/core/kernel/events.js +101 -0
  14. package/core/kernel/flow.js +22 -0
  15. package/core/kernel/index.js +17 -0
  16. package/core/kernel/plugin.js +23 -0
  17. package/core/kernel/plugins/sample-seo.js +26 -0
  18. package/core/kernel/run-demo.js +58 -0
  19. package/core/kernel/view.js +167 -0
  20. package/core/openapi/build-from-api-routes.js +8 -2
  21. package/core/orm/model.js +3 -1
  22. package/core/url-path-normalize.js +30 -0
  23. package/index.d.ts +168 -1
  24. package/index.js +20 -2
  25. package/package.json +11 -1
  26. package/plugins/admin-panel/api.js +43 -15
  27. package/plugins/admin-panel/app.js +109 -0
  28. package/plugins/admin-panel/client/README.md +39 -0
  29. package/plugins/admin-panel/client/load-parts.js +74 -0
  30. package/plugins/admin-panel/client/manifest.parts.json +12 -0
  31. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +150 -0
  32. package/plugins/admin-panel/client/parts/02-filter-components.js +554 -0
  33. package/plugins/admin-panel/client/parts/03-pagination-intro.js +70 -0
  34. package/plugins/admin-panel/client/parts/04-field-renderers.js +287 -0
  35. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +335 -0
  36. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +125 -0
  37. package/plugins/admin-panel/client/parts/07-model-list.js +596 -0
  38. package/plugins/admin-panel/client/parts/08-record-list.js +536 -0
  39. package/plugins/admin-panel/client/parts/09-record-form.js +170 -0
  40. package/plugins/admin-panel/client/parts/10-export-registry.js +11 -0
  41. package/plugins/admin-panel/client/verify-spa-parts.js +32 -0
  42. package/plugins/admin-panel/client/vite.config.example.mjs +22 -0
  43. package/plugins/admin-panel/components.js +4 -2640
  44. package/plugins/admin-panel/core/api-extensions.js +100 -10
  45. package/plugins/admin-panel/index.js +3 -0
  46. package/plugins/admin-panel/lib/is-rich-text-empty.js +23 -0
  47. package/plugins/admin-panel/lib/sanitize-rich-html.js +106 -0
  48. package/plugins/admin-panel/modules/dashboard.js +17 -13
  49. package/plugins/admin-panel/modules/user-management.js +118 -27
  50. package/plugins/data-exchange/export-xlsx.js +3 -0
  51. package/plugins/data-exchange/record-selection.js +21 -5
  52. package/plugins/index.js +4 -0
  53. package/plugins/rate-limit/index.js +178 -0
  54. package/plugins/redirect/index.js +204 -0
  55. package/plugins/rest-resources/index.js +2 -1
  56. package/plugins/site-analytics/admin-component.js +88 -78
  57. package/plugins/swagger.js +2 -1
  58. package/plugins/upload/local-file-provider.js +6 -2
  59. package/src/file-router.js +270 -53
  60. package/src/njk-frontmatter.js +156 -0
  61. package/src/plugin-manager.js +4 -2
  62. package/src/server.js +28 -9
  63. package/templates/skills/webspresso-usage/REFERENCE-framework.md +276 -0
  64. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +93 -0
  65. package/templates/skills/webspresso-usage/SKILL.md +29 -275
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Load ORM model files into the global registry without opening a DB connection.
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { resolveDbConfigIfExists } = require('./db');
8
+ const { getWebspressoOrmForProject } = require('./resolve-webspresso-orm');
9
+
10
+ /**
11
+ * Resolve absolute models directory.
12
+ * @param {string} cwd
13
+ * @param {{ modelsOverride?: string, configPath?: string, env?: string }} options
14
+ * @returns {string}
15
+ */
16
+ function resolveModelsDir(cwd, options = {}) {
17
+ const { modelsOverride, configPath, env } = options;
18
+ if (modelsOverride) {
19
+ return path.resolve(cwd, modelsOverride);
20
+ }
21
+
22
+ const resolved = resolveDbConfigIfExists(configPath);
23
+ if (resolved) {
24
+ const environment = env || process.env.NODE_ENV || 'development';
25
+ const cfg = resolved.config[environment] ?? resolved.config;
26
+ if (cfg && typeof cfg.models === 'string') {
27
+ return path.resolve(cwd, cfg.models);
28
+ }
29
+ }
30
+
31
+ return path.resolve(cwd, 'models');
32
+ }
33
+
34
+ /**
35
+ * Require each model file (same filter as createDatabase).
36
+ * @param {string} modelsDir
37
+ * @returns {{ loaded: string[], errors: Array<{ file: string, message: string }> }}
38
+ */
39
+ function loadModelFiles(modelsDir) {
40
+ const errors = [];
41
+ if (!fs.existsSync(modelsDir)) {
42
+ return {
43
+ loaded: [],
44
+ errors: [{ file: '', message: `Models directory not found: ${modelsDir}` }],
45
+ };
46
+ }
47
+
48
+ const files = fs
49
+ .readdirSync(modelsDir)
50
+ .filter((f) => f.endsWith('.js') && !f.startsWith('_'))
51
+ .sort();
52
+
53
+ const loaded = [];
54
+ for (const file of files) {
55
+ const full = path.join(modelsDir, file);
56
+ try {
57
+ require(full);
58
+ loaded.push(file);
59
+ } catch (e) {
60
+ errors.push({ file, message: e.message || String(e) });
61
+ }
62
+ }
63
+
64
+ return { loaded, errors };
65
+ }
66
+
67
+ /**
68
+ * Clear registry and load all models from the resolved directory.
69
+ * @param {string} cwd
70
+ * @param {{ modelsOverride?: string, configPath?: string, env?: string }} options
71
+ * @returns {{ modelsDir: string, loaded: string[], errors: Array<{ file: string, message: string }> }}
72
+ */
73
+ function loadProjectModels(cwd, options = {}) {
74
+ const orm = getWebspressoOrmForProject(cwd);
75
+ orm.clearRegistry();
76
+ const modelsDir = resolveModelsDir(cwd, options);
77
+ const { loaded, errors } = loadModelFiles(modelsDir);
78
+ return { modelsDir, loaded, errors };
79
+ }
80
+
81
+ module.exports = {
82
+ resolveModelsDir,
83
+ loadModelFiles,
84
+ loadProjectModels,
85
+ };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Serialize ORM registry to JSON-safe snapshot + Mermaid erDiagram source.
3
+ */
4
+
5
+ /**
6
+ * @param {string} name
7
+ * @returns {string}
8
+ */
9
+ function mermaidEntityId(name) {
10
+ return String(name).replace(/[^a-zA-Z0-9_]/g, '_');
11
+ }
12
+
13
+ /**
14
+ * @param {import('../../../core/orm/types').ColumnMeta} meta
15
+ * @returns {Record<string, unknown>}
16
+ */
17
+ function sanitizeColumnMeta(meta) {
18
+ if (!meta || typeof meta !== 'object') return {};
19
+ const out = {};
20
+ const keys = [
21
+ 'type',
22
+ 'nullable',
23
+ 'primary',
24
+ 'autoIncrement',
25
+ 'unique',
26
+ 'index',
27
+ 'default',
28
+ 'maxLength',
29
+ 'precision',
30
+ 'scale',
31
+ 'enumValues',
32
+ 'references',
33
+ 'referenceColumn',
34
+ 'auto',
35
+ ];
36
+ for (const k of keys) {
37
+ if (meta[k] !== undefined) out[k] = meta[k];
38
+ }
39
+ if (meta.validations && typeof meta.validations === 'object') {
40
+ out.validations = { ...meta.validations };
41
+ }
42
+ if (meta.ui && typeof meta.ui === 'object') {
43
+ out.ui = { ...meta.ui };
44
+ }
45
+ return out;
46
+ }
47
+
48
+ /**
49
+ * @param {import('../../../core/orm/types').ModelDefinition} def
50
+ * @returns {object}
51
+ */
52
+ function serializeModel(def) {
53
+ const columns = [];
54
+ for (const [colName, meta] of def.columns) {
55
+ columns.push({
56
+ name: colName,
57
+ ...sanitizeColumnMeta(meta),
58
+ });
59
+ }
60
+ columns.sort((a, b) => a.name.localeCompare(b.name));
61
+
62
+ const relations = [];
63
+ for (const [relName, rel] of Object.entries(def.relations || {})) {
64
+ let targetModel = null;
65
+ try {
66
+ if (typeof rel.model === 'function') {
67
+ const tgt = rel.model();
68
+ targetModel = tgt && tgt.name ? tgt.name : null;
69
+ }
70
+ } catch {
71
+ targetModel = null;
72
+ }
73
+ relations.push({
74
+ name: relName,
75
+ type: rel.type,
76
+ foreignKey: rel.foreignKey,
77
+ localKey: rel.localKey || 'id',
78
+ targetModel,
79
+ });
80
+ }
81
+ relations.sort((a, b) => a.name.localeCompare(b.name));
82
+
83
+ let cache = def.cache;
84
+ if (cache !== undefined && typeof cache === 'object' && cache !== null) {
85
+ try {
86
+ JSON.stringify(cache);
87
+ } catch {
88
+ cache = '[object]';
89
+ }
90
+ }
91
+
92
+ return {
93
+ name: def.name,
94
+ table: def.table,
95
+ primaryKey: def.primaryKey,
96
+ scopes: { ...def.scopes },
97
+ hidden: [...def.hidden],
98
+ admin: {
99
+ enabled: def.admin.enabled,
100
+ label: def.admin.label,
101
+ icon: def.admin.icon,
102
+ },
103
+ rest: {
104
+ enabled: def.rest.enabled,
105
+ path: def.rest.path,
106
+ allowInclude: def.rest.allowInclude,
107
+ },
108
+ columns,
109
+ relations,
110
+ cache,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * @param {Map<string, import('../../../core/orm/types').ModelDefinition>} modelsMap
116
+ * @param {{ generatedAt?: string }} [opts]
117
+ * @returns {{ generatedAt: string, models: object[] }}
118
+ */
119
+ function buildSnapshot(modelsMap, opts = {}) {
120
+ const models = [];
121
+ for (const [, def] of modelsMap) {
122
+ models.push(serializeModel(def));
123
+ }
124
+ models.sort((a, b) => a.name.localeCompare(b.name));
125
+ return {
126
+ generatedAt: opts.generatedAt || new Date().toISOString(),
127
+ models,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Mermaid erDiagram: entity blocks + relationship lines (deduped).
133
+ * @param {{ models: object[] }} snapshot
134
+ * @returns {string}
135
+ */
136
+ function buildMermaidErDiagram(snapshot) {
137
+ const lines = ['erDiagram'];
138
+
139
+ for (const m of snapshot.models) {
140
+ const id = mermaidEntityId(m.name);
141
+ lines.push(` ${id} {`);
142
+ for (const col of m.columns) {
143
+ const t = col.type != null ? String(col.type) : 'unknown';
144
+ const nm = col.name.replace(/[^\w]/g, '_');
145
+ lines.push(` ${t} ${nm}`);
146
+ }
147
+ lines.push(' }');
148
+ }
149
+
150
+ const seen = new Set();
151
+ for (const m of snapshot.models) {
152
+ const srcId = mermaidEntityId(m.name);
153
+ for (const r of m.relations) {
154
+ if (!r.targetModel) continue;
155
+ const tgtId = mermaidEntityId(r.targetModel);
156
+ let mid;
157
+ if (r.type === 'hasMany') mid = `${srcId} ||--o{ ${tgtId}`;
158
+ else if (r.type === 'hasOne') mid = `${srcId} ||--|| ${tgtId}`;
159
+ else if (r.type === 'belongsTo') mid = `${srcId} }o--|| ${tgtId}`;
160
+ else continue;
161
+
162
+ const label = String(r.name).replace(/"/g, "'");
163
+ const full = `${mid} : "${label}"`;
164
+ const key = `${srcId}|${tgtId}|${mid}`;
165
+ if (seen.has(key)) continue;
166
+ seen.add(key);
167
+ lines.push(` ${full}`);
168
+ }
169
+ }
170
+
171
+ return lines.join('\n');
172
+ }
173
+
174
+ module.exports = {
175
+ mermaidEntityId,
176
+ buildSnapshot,
177
+ buildMermaidErDiagram,
178
+ serializeModel,
179
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Resolve the same webspresso/core/orm instance that app model files use
3
+ * when they `require('webspresso')`. Global CLI would otherwise load a
4
+ * different module copy than the project's node_modules.
5
+ */
6
+
7
+ const path = require('path');
8
+
9
+ /**
10
+ * @param {string} cwd - Project root (typically `process.cwd()`).
11
+ * @returns {typeof import('../../core/orm')}
12
+ */
13
+ function getWebspressoOrmForProject(cwd) {
14
+ try {
15
+ const pkgJson = require.resolve('webspresso/package.json', { paths: [cwd] });
16
+ const root = path.dirname(pkgJson);
17
+ return require(path.join(root, 'core', 'orm'));
18
+ } catch {
19
+ return require(path.join(__dirname, '../../core/orm'));
20
+ }
21
+ }
22
+
23
+ module.exports = { getWebspressoOrmForProject };
package/bin/webspresso.js CHANGED
@@ -30,6 +30,8 @@ const { registerCommand: registerFaviconGenerate } = require('./commands/favicon
30
30
  const { registerCommand: registerAuditPrune } = require('./commands/audit-prune');
31
31
  const { registerCommand: registerDoctor } = require('./commands/doctor');
32
32
  const { registerCommand: registerSkill } = require('./commands/skill');
33
+ const { registerCommand: registerUpgrade } = require('./commands/upgrade');
34
+ const { registerCommand: registerOrmMap } = require('./commands/orm-map');
33
35
 
34
36
  registerNew(program);
35
37
  registerPage(program);
@@ -48,6 +50,8 @@ registerFaviconGenerate(program);
48
50
  registerAuditPrune(program);
49
51
  registerDoctor(program);
50
52
  registerSkill(program);
53
+ registerUpgrade(program);
54
+ registerOrmMap(program);
51
55
 
52
56
  // Parse arguments
53
57
  program.parse();
@@ -116,7 +116,20 @@ class AuthManager {
116
116
  if (!config.secret) {
117
117
  throw new Error('Session secret is required. Set AUTH_SESSION_SECRET environment variable or pass session.secret in config.');
118
118
  }
119
-
119
+
120
+ const mergedCookie = {
121
+ ...DEFAULT_CONFIG.session.cookie,
122
+ ...(config.cookie || {}),
123
+ };
124
+ const userSetSecure =
125
+ typeof (this.config.session.cookie && this.config.session.cookie.secure) === 'boolean';
126
+ const secureComputed =
127
+ process.env.NODE_ENV === 'production' ||
128
+ process.env.COOKIE_SECURE === 'true' ||
129
+ /^https:/i.test(String(process.env.BASE_URL || '').trim());
130
+ mergedCookie.secure = userSetSecure ? mergedCookie.secure : secureComputed;
131
+ config.cookie = mergedCookie;
132
+
120
133
  return config;
121
134
  }
122
135
 
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Minimal extensible application kernel (event bus, views, flows, plugins).
3
+ * @module core/kernel/app
4
+ */
5
+
6
+ const { createEventBus } = require('./events');
7
+ const { createViewEngine } = require('./view');
8
+ const { definePlugin } = require('./plugin');
9
+ const { defineFlow } = require('./flow');
10
+ const { BaseRepository } = require('./base-repository');
11
+
12
+ /**
13
+ * @typedef {{
14
+ * events: ReturnType<typeof createEventBus>,
15
+ * view: ReturnType<typeof createViewEngine>,
16
+ * flows: Array<{ id?: string, trigger: string }>,
17
+ * registerPlugin: (plugin: Parameters<typeof definePlugin>[0]) => void,
18
+ * registerFlow: (flow: ReturnType<typeof defineFlow>) => () => void,
19
+ * paths: Record<string, string | undefined>,
20
+ * }} KernelApp
21
+ */
22
+
23
+ /**
24
+ * @param {{
25
+ * paths?: { appViews?: string, themeViews?: string },
26
+ * }} [options]
27
+ */
28
+ function createApp(options = {}) {
29
+ const paths = options.paths || {};
30
+ const events = createEventBus();
31
+ const view = createViewEngine({
32
+ appViews: paths.appViews,
33
+ themeViews: paths.themeViews,
34
+ });
35
+
36
+ /** @type {Array<{ id?: string, trigger: string, unregister: () => void }>} */
37
+ const flowHandles = [];
38
+
39
+ /**
40
+ * @param {ReturnType<typeof defineFlow>} flowDef
41
+ */
42
+ function registerFlow(flowDef) {
43
+ const id = flowDef.id || flowDef.trigger;
44
+ /** @param {import('./events').KernelEventContext} ctx */
45
+ const handler = async (ctx) => {
46
+ if (flowDef.when && !flowDef.when(ctx)) return;
47
+ const actions = flowDef.actions || [];
48
+ for (const action of actions) {
49
+ await action(ctx, app);
50
+ }
51
+ };
52
+ events.on(flowDef.trigger, handler);
53
+ const unregister = () => events.off(flowDef.trigger, handler);
54
+ flowHandles.push({
55
+ id,
56
+ trigger: flowDef.trigger,
57
+ unregister,
58
+ });
59
+ return unregister;
60
+ }
61
+
62
+ /**
63
+ * @param {Parameters<typeof definePlugin>[0]} plugin
64
+ */
65
+ function registerPlugin(plugin) {
66
+ if (typeof plugin.views === 'function') {
67
+ const bundle = plugin.views();
68
+ view.registerPluginViews(plugin.name, bundle);
69
+ }
70
+ if (typeof plugin.events === 'function') {
71
+ plugin.events(app);
72
+ }
73
+ }
74
+
75
+ /** @type {KernelApp} */
76
+ const app = {
77
+ events,
78
+ view,
79
+ options,
80
+ paths,
81
+ get flows() {
82
+ return flowHandles.map(({ id, trigger }) => ({ id, trigger }));
83
+ },
84
+ registerPlugin,
85
+ registerFlow,
86
+ };
87
+
88
+ return app;
89
+ }
90
+
91
+ module.exports = {
92
+ createApp,
93
+ definePlugin,
94
+ defineFlow,
95
+ BaseRepository,
96
+ };
@@ -0,0 +1,143 @@
1
+ const { randomUUID } = require('./events');
2
+
3
+ /**
4
+ * @typedef {ReturnType<import('./events').createEventBus>} EventBus
5
+ */
6
+
7
+ /**
8
+ * Simulated in-memory row store keyed by id (UUID).
9
+ */
10
+ class MemoryStore {
11
+ constructor() {
12
+ /** @type {Array<{ id: string } & Record<string, any>>} */
13
+ this._rows = [];
14
+ }
15
+
16
+ insert(record) {
17
+ this._rows.push(record);
18
+ return record;
19
+ }
20
+
21
+ findIndex(id) {
22
+ return this._rows.findIndex((r) => r.id === id);
23
+ }
24
+
25
+ getAt(index) {
26
+ return this._rows[index];
27
+ }
28
+
29
+ /**
30
+ * @param {number} index
31
+ * @param {{ id: string } & Record<string, any>} row
32
+ */
33
+ setAt(index, row) {
34
+ this._rows[index] = row;
35
+ }
36
+
37
+ deleteAt(index) {
38
+ const [removed] = this._rows.splice(index, 1);
39
+ return removed;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @template T
45
+ */
46
+ class BaseRepository {
47
+ /**
48
+ * @param {EventBus} events
49
+ * @param {{ resource: string, source?: import('./events').EventSource }} options
50
+ */
51
+ constructor(events, options) {
52
+ this.events = events;
53
+ this.resource = options.resource;
54
+ this.source = options.source || 'orm';
55
+ this._store = new MemoryStore();
56
+ }
57
+
58
+ /** @param {string} phase */
59
+ _eventName(phase) {
60
+ return `orm.${this.resource}.${phase}`;
61
+ }
62
+
63
+ /**
64
+ * @param {T & { id?: string }} data
65
+ */
66
+ async create(data) {
67
+ const id = data.id || randomUUID();
68
+ const record = { ...data, id };
69
+
70
+ const beforeCtx = this.events.buildContext(
71
+ { data: { ...record } },
72
+ { source: this.source },
73
+ );
74
+ await this.events.dispatch(this._eventName('beforeCreate'), beforeCtx);
75
+
76
+ const toInsert = beforeCtx.payload.data;
77
+ const saved = this._store.insert({ ...toInsert });
78
+
79
+ const afterCtx = this.events.buildContext(
80
+ { record: saved },
81
+ { source: this.source },
82
+ );
83
+ await this.events.publish(this._eventName('afterCreate'), afterCtx);
84
+
85
+ return saved;
86
+ }
87
+
88
+ /**
89
+ * @param {string} id
90
+ * @param {Partial<T>} data
91
+ */
92
+ async update(id, data) {
93
+ const idx = this._store.findIndex(id);
94
+ if (idx === -1) {
95
+ throw new Error(`Record not found: ${id}`);
96
+ }
97
+ const existing = this._store.getAt(idx);
98
+
99
+ const beforeCtx = this.events.buildContext(
100
+ { id, data, record: { ...existing } },
101
+ { source: this.source },
102
+ );
103
+ await this.events.dispatch(this._eventName('beforeUpdate'), beforeCtx);
104
+
105
+ const merged = { ...existing, ...beforeCtx.payload.data, id };
106
+ this._store.setAt(idx, merged);
107
+
108
+ const afterCtx = this.events.buildContext(
109
+ { record: merged, previous: existing },
110
+ { source: this.source },
111
+ );
112
+ await this.events.publish(this._eventName('afterUpdate'), afterCtx);
113
+
114
+ return merged;
115
+ }
116
+
117
+ /**
118
+ * @param {string} id
119
+ */
120
+ async delete(id) {
121
+ const idx = this._store.findIndex(id);
122
+ if (idx === -1) {
123
+ throw new Error(`Record not found: ${id}`);
124
+ }
125
+ const existing = this._store.getAt(idx);
126
+
127
+ const beforeCtx = this.events.buildContext(
128
+ { id, record: { ...existing } },
129
+ { source: this.source },
130
+ );
131
+ await this.events.dispatch(this._eventName('beforeDelete'), beforeCtx);
132
+
133
+ const removed = this._store.deleteAt(idx);
134
+
135
+ const afterCtx = this.events.buildContext(
136
+ { id, record: removed },
137
+ { source: this.source },
138
+ );
139
+ await this.events.publish(this._eventName('afterDelete'), afterCtx);
140
+ }
141
+ }
142
+
143
+ module.exports = { BaseRepository, MemoryStore };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Application event bus: sync dispatch vs async publish.
3
+ * @module core/kernel/events
4
+ */
5
+
6
+ /**
7
+ * @typedef {'orm' | 'auth' | 'route' | 'plugin' | 'system'} EventSource
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} EventMeta
12
+ * @property {string} [requestId]
13
+ * @property {string} [userId]
14
+ * @property {EventSource} source
15
+ * @property {Date} createdAt
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} KernelEventContext
20
+ * @property {any} payload
21
+ * @property {EventMeta} meta
22
+ */
23
+
24
+ const { randomUUID } = require('crypto');
25
+
26
+ /**
27
+ * @param {any} payload
28
+ * @param {{ source: EventSource, requestId?: string, userId?: string }} meta
29
+ * @returns {KernelEventContext}
30
+ */
31
+ function buildContext(payload, meta) {
32
+ return {
33
+ payload,
34
+ meta: {
35
+ source: meta.source,
36
+ requestId: meta.requestId,
37
+ userId: meta.userId,
38
+ createdAt: new Date(),
39
+ },
40
+ };
41
+ }
42
+
43
+ /**
44
+ * @returns {{ dispatch: Function, publish: Function, on: Function, off: Function, buildContext: typeof buildContext }}
45
+ */
46
+ function createEventBus() {
47
+ /** @type {Map<string, Array<(ctx: KernelEventContext) => any>>} */
48
+ const listeners = new Map();
49
+
50
+ /**
51
+ * @param {string} eventName
52
+ * @param {(ctx: KernelEventContext) => any} handler
53
+ */
54
+ function on(eventName, handler) {
55
+ if (!listeners.has(eventName)) {
56
+ listeners.set(eventName, []);
57
+ }
58
+ listeners.get(eventName).push(handler);
59
+ }
60
+
61
+ /**
62
+ * @param {string} eventName
63
+ * @param {(ctx: KernelEventContext) => any} handler
64
+ */
65
+ function off(eventName, handler) {
66
+ const list = listeners.get(eventName);
67
+ if (!list) return;
68
+ const i = list.indexOf(handler);
69
+ if (i !== -1) list.splice(i, 1);
70
+ }
71
+
72
+ /**
73
+ * @param {string} eventName
74
+ * @param {KernelEventContext} ctx
75
+ */
76
+ async function dispatch(eventName, ctx) {
77
+ const list = listeners.get(eventName) || [];
78
+ let last;
79
+ for (const fn of list) {
80
+ last = await fn(ctx);
81
+ }
82
+ return last;
83
+ }
84
+
85
+ /**
86
+ * @param {string} eventName
87
+ * @param {KernelEventContext} ctx
88
+ */
89
+ async function publish(eventName, ctx) {
90
+ const list = listeners.get(eventName) || [];
91
+ await Promise.all(list.map((fn) => Promise.resolve(fn(ctx))));
92
+ }
93
+
94
+ return { dispatch, publish, on, off, buildContext };
95
+ }
96
+
97
+ module.exports = {
98
+ createEventBus,
99
+ buildContext,
100
+ randomUUID,
101
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @module core/kernel/flow
3
+ */
4
+
5
+ /**
6
+ * @param {{
7
+ * id?: string,
8
+ * trigger: string,
9
+ * when?: (ctx: import('./events').KernelEventContext) => boolean,
10
+ * actions: Array<
11
+ * (
12
+ * ctx: import('./events').KernelEventContext,
13
+ * app: Record<string, unknown>,
14
+ * ) => Promise<void> | void
15
+ * >,
16
+ * }} definition
17
+ */
18
+ function defineFlow(definition) {
19
+ return definition;
20
+ }
21
+
22
+ module.exports = { defineFlow };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Application kernel: event bus, simulated repository hooks, plugins, views, flows.
3
+ * @module core/kernel
4
+ */
5
+
6
+ module.exports = {
7
+ createApp: require('./app').createApp,
8
+ definePlugin: require('./plugin').definePlugin,
9
+ defineFlow: require('./flow').defineFlow,
10
+ BaseRepository: require('./base-repository').BaseRepository,
11
+ createEventBus: require('./events').createEventBus,
12
+ buildContext: require('./events').buildContext,
13
+ randomUUID: require('./events').randomUUID,
14
+ createViewEngine: require('./view').createViewEngine,
15
+ renderTemplate: require('./view').renderTemplate,
16
+ parseQualified: require('./view').parseQualified,
17
+ };