webspresso 0.0.74 → 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 (60) hide show
  1. package/README.md +41 -3
  2. package/bin/commands/orm-map.js +139 -0
  3. package/bin/commands/skill.js +22 -8
  4. package/bin/utils/orm-map-html.js +689 -0
  5. package/bin/utils/orm-map-load.js +85 -0
  6. package/bin/utils/orm-map-snapshot.js +179 -0
  7. package/bin/utils/resolve-webspresso-orm.js +23 -0
  8. package/bin/webspresso.js +2 -0
  9. package/core/auth/manager.js +14 -1
  10. package/core/kernel/app.js +96 -0
  11. package/core/kernel/base-repository.js +143 -0
  12. package/core/kernel/events.js +101 -0
  13. package/core/kernel/flow.js +22 -0
  14. package/core/kernel/index.js +17 -0
  15. package/core/kernel/plugin.js +23 -0
  16. package/core/kernel/plugins/sample-seo.js +26 -0
  17. package/core/kernel/run-demo.js +58 -0
  18. package/core/kernel/view.js +167 -0
  19. package/core/openapi/build-from-api-routes.js +8 -2
  20. package/core/orm/model.js +3 -1
  21. package/core/url-path-normalize.js +30 -0
  22. package/index.d.ts +168 -1
  23. package/index.js +20 -2
  24. package/package.json +11 -1
  25. package/plugins/admin-panel/api.js +43 -15
  26. package/plugins/admin-panel/client/README.md +39 -0
  27. package/plugins/admin-panel/client/load-parts.js +74 -0
  28. package/plugins/admin-panel/client/manifest.parts.json +12 -0
  29. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +150 -0
  30. package/plugins/admin-panel/client/parts/02-filter-components.js +554 -0
  31. package/plugins/admin-panel/client/parts/03-pagination-intro.js +70 -0
  32. package/plugins/admin-panel/client/parts/04-field-renderers.js +287 -0
  33. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +335 -0
  34. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +125 -0
  35. package/plugins/admin-panel/client/parts/07-model-list.js +596 -0
  36. package/plugins/admin-panel/client/parts/08-record-list.js +536 -0
  37. package/plugins/admin-panel/client/parts/09-record-form.js +170 -0
  38. package/plugins/admin-panel/client/parts/10-export-registry.js +11 -0
  39. package/plugins/admin-panel/client/verify-spa-parts.js +32 -0
  40. package/plugins/admin-panel/client/vite.config.example.mjs +22 -0
  41. package/plugins/admin-panel/components.js +4 -2640
  42. package/plugins/admin-panel/core/api-extensions.js +100 -10
  43. package/plugins/admin-panel/index.js +3 -0
  44. package/plugins/admin-panel/lib/is-rich-text-empty.js +23 -0
  45. package/plugins/admin-panel/lib/sanitize-rich-html.js +106 -0
  46. package/plugins/admin-panel/modules/dashboard.js +3 -2
  47. package/plugins/admin-panel/modules/user-management.js +90 -20
  48. package/plugins/index.js +4 -0
  49. package/plugins/rate-limit/index.js +178 -0
  50. package/plugins/redirect/index.js +204 -0
  51. package/plugins/rest-resources/index.js +2 -1
  52. package/plugins/swagger.js +2 -1
  53. package/plugins/upload/local-file-provider.js +6 -2
  54. package/src/file-router.js +191 -50
  55. package/src/njk-frontmatter.js +156 -0
  56. package/src/plugin-manager.js +4 -2
  57. package/src/server.js +26 -9
  58. package/templates/skills/webspresso-usage/REFERENCE-framework.md +276 -0
  59. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +93 -0
  60. package/templates/skills/webspresso-usage/SKILL.md +29 -278
@@ -0,0 +1,170 @@
1
+ // Record Form Component - renders fields based on model schema
2
+ const RecordForm = {
3
+ oninit: () => {
4
+ const modelName = m.route.param('model');
5
+ const id = m.route.param('id');
6
+ state.error = null;
7
+ state.loading = true;
8
+ state.formData = {};
9
+ state.currentModelMeta = null;
10
+
11
+ // Load model metadata first
12
+ api.get('/models/' + modelName)
13
+ .then(modelMeta => {
14
+ state.currentModelMeta = modelMeta;
15
+
16
+ // Initialize form data with defaults
17
+ modelMeta.columns.forEach(col => {
18
+ if (col.default !== undefined) {
19
+ state.formData[col.name] = col.default;
20
+ }
21
+ });
22
+
23
+ // If editing, load the record
24
+ if (id && id !== 'new') {
25
+ return api.get('/models/' + modelName + '/records/' + id)
26
+ .then(result => {
27
+ state.currentRecord = result.data;
28
+ // Populate form data with record values
29
+ Object.keys(result.data).forEach(key => {
30
+ state.formData[key] = result.data[key];
31
+ });
32
+ });
33
+ } else {
34
+ state.currentRecord = null;
35
+ }
36
+ })
37
+ .catch(err => {
38
+ state.error = err.message;
39
+ })
40
+ .finally(() => {
41
+ state.loading = false;
42
+ m.redraw();
43
+ });
44
+ },
45
+ view: () => {
46
+ const modelName = m.route.param('model');
47
+ const id = m.route.param('id');
48
+ const isNew = !id || id === 'new';
49
+ const modelMeta = state.currentModelMeta;
50
+
51
+ const breadcrumbs = [
52
+ { label: modelMeta?.label || modelName, href: '/models/' + modelName },
53
+ { label: isNew ? 'New' : 'Edit #' + id, href: '#' },
54
+ ];
55
+
56
+ return m(Layout, { breadcrumbs }, [
57
+ m('.flex.items-center.justify-between.mb-6', [
58
+ m('h2.text-2xl.font-bold.text-gray-900.dark:text-slate-100', isNew ? 'New Record' : 'Edit Record'),
59
+ modelMeta ? m('span.text-gray-500.dark:text-slate-400', modelMeta.label || modelMeta.name) : null,
60
+ ]),
61
+
62
+ state.loading ? m('p.text-gray-600.dark:text-slate-400', 'Loading...') :
63
+ state.error && !modelMeta ? m('.bg-red-50.dark:bg-red-950/40.border.border-red-200.dark:border-red-800.text-red-800.dark:text-red-200.px-4.py-3.rounded-lg', state.error) :
64
+
65
+ m('form.bg-white.dark:bg-slate-800.rounded-lg.shadow-sm.border.border-gray-200.dark:border-slate-700.flex.flex-col', {
66
+ style: 'min-height: calc(100vh - 280px);',
67
+ onsubmit: async (e) => {
68
+ e.preventDefault();
69
+ state.loading = true;
70
+ state.error = null;
71
+ try {
72
+ // Validate rich-text fields first
73
+ if (modelMeta && modelMeta.columns) {
74
+ for (const col of modelMeta.columns) {
75
+ if (col.customField && col.customField.type === 'rich-text' && !col.nullable) {
76
+ const hiddenInput = document.getElementById(col.name + '-value');
77
+ const value = hiddenInput ? hiddenInput.value : state.formData[col.name];
78
+ if (isRichTextEmpty(value)) {
79
+ state.error = (col.ui?.label || col.name) + ' is required';
80
+ state.loading = false;
81
+ return;
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ // Build payload, excluding auto-generated fields
88
+ const payload = {};
89
+ if (modelMeta && modelMeta.columns) {
90
+ modelMeta.columns.forEach(col => {
91
+ const autoType = isAutoColumn(col);
92
+ // Skip primary key and auto timestamps in payload
93
+ if (autoType === 'primary' || autoType === 'auto') return;
94
+
95
+ // For rich-text fields, get value from hidden input
96
+ let value = state.formData[col.name];
97
+ if (col.customField && col.customField.type === 'rich-text') {
98
+ const hiddenInput = document.getElementById(col.name + '-value');
99
+ if (hiddenInput) {
100
+ value = hiddenInput.value;
101
+ }
102
+ // Skip empty rich-text values (normalize to null if nullable)
103
+ if (isRichTextEmpty(value)) {
104
+ if (col.nullable) {
105
+ payload[col.name] = null;
106
+ }
107
+ return;
108
+ }
109
+ }
110
+
111
+ if (value !== undefined && value !== null && value !== '') {
112
+ payload[col.name] = value;
113
+ } else if (value === null && col.nullable) {
114
+ payload[col.name] = null;
115
+ }
116
+ });
117
+ }
118
+
119
+ if (isNew) {
120
+ await api.post('/models/' + modelName + '/records', payload);
121
+ } else {
122
+ await api.put('/models/' + modelName + '/records/' + id, payload);
123
+ }
124
+ m.route.set('/models/' + modelName);
125
+ } catch (err) {
126
+ state.error = err.message;
127
+ } finally {
128
+ state.loading = false;
129
+ }
130
+ }
131
+ }, [
132
+ // Form content (scrollable)
133
+ m('.p-6.flex-1.overflow-y-auto', [
134
+ state.error ? m('.bg-red-50.dark:bg-red-950/40.border.border-red-200.dark:border-red-800.text-red-800.dark:text-red-200.px-4.py-3.rounded-lg.mb-4', state.error) : null,
135
+
136
+ // Render form fields based on model columns
137
+ modelMeta && modelMeta.columns ? modelMeta.columns.map(col => {
138
+ const autoType = isAutoColumn(col);
139
+
140
+ // Hide primary key in new mode
141
+ if (autoType === 'primary' && isNew) return null;
142
+
143
+ // Hide hidden fields
144
+ if (col.ui && col.ui.hidden) return null;
145
+
146
+ const isReadonly = !!autoType || (col.ui && col.ui.readonly);
147
+ const renderer = getFieldRenderer(col, modelMeta);
148
+ const value = state.formData[col.name];
149
+ const onChange = (newValue) => {
150
+ state.formData[col.name] = newValue;
151
+ };
152
+
153
+ return renderer(col, value, onChange, isReadonly);
154
+ }) : m('p.text-gray-600 dark:text-slate-400.mb-4', 'Loading form fields...'),
155
+ ]),
156
+
157
+ // Sticky footer buttons
158
+ m('.flex.gap-4.p-4.border-t.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900.sticky.bottom-0', [
159
+ m('button.bg-blue-600.dark:bg-blue-500.text-white.px-6.py-2.rounded-lg.hover:bg-blue-700.dark:hover:bg-blue-600.disabled:opacity-50', {
160
+ type: 'submit',
161
+ disabled: state.loading,
162
+ }, state.loading ? 'Saving...' : 'Save'),
163
+ m('button.bg-gray-200 dark:bg-slate-700.text-gray-800 dark:text-slate-200.px-6.py-2.rounded.hover:bg-gray-300 dark:hover:bg-slate-600 dark:hover:bg-slate-600[type=button]', {
164
+ onclick: () => m.route.set('/models/' + modelName),
165
+ }, 'Cancel'),
166
+ ]),
167
+ ]),
168
+ ]);
169
+ },
170
+ };
@@ -0,0 +1,11 @@
1
+ // Export components
2
+ window.__ADMIN_COMPONENTS__ = {
3
+ LoginForm,
4
+ SetupForm,
5
+ Layout,
6
+ ModelList,
7
+ RecordList,
8
+ RecordForm,
9
+ api,
10
+ state,
11
+ };
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sanity check: manifest + parts load and look like the admin SPA bundle.
4
+ * Run from repo root: node plugins/admin-panel/client/verify-spa-parts.js
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const path = require('path');
10
+ const { buildComponentsBody } = require(path.join(__dirname, 'load-parts.js'));
11
+
12
+ const body = buildComponentsBody();
13
+ const checks = [
14
+ ['has api helper', () => body.includes('const api =')],
15
+ ['has Mithril globals', () => body.includes('window.__ADMIN_COMPONENTS__')],
16
+ ['has reasonable size', () => body.length > 50_000],
17
+ ];
18
+
19
+ let failed = false;
20
+ for (const [label, ok] of checks) {
21
+ if (!ok()) {
22
+ console.error('FAIL:', label);
23
+ failed = true;
24
+ }
25
+ }
26
+ if (failed) {
27
+ process.exit(1);
28
+ }
29
+ console.log(
30
+ `OK admin SPA bundle (${checks.length} checks), length=${body.length} bytes.`,
31
+ );
32
+ process.exit(0);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Optional Vite scaffold — NOT used by npm install or CI.
3
+ *
4
+ * Typical use: concatenate `parts/*.js` according to manifest.parts.json using a
5
+ * small plugin or a pre-build shell script that writes `dist/admin-spa.iife.js`, then
6
+ * point your experiment at that asset. Replacing inlined HTML `<script>...</script>`
7
+ * requires changes in index.js generateAdminPanelHtml().
8
+ *
9
+ * @example npm install vite --prefix plugins/admin-panel/client --no-save
10
+ */
11
+
12
+ // import { defineConfig } from 'vite';
13
+ //
14
+ // export default defineConfig({
15
+ // root: import.meta.dirname,
16
+ // build: {
17
+ // outDir: 'dist',
18
+ // emptyOutDir: true,
19
+ // rollupOptions: {},
20
+ // lib: { entry: 'parts-placeholder.js', name: '_', formats: ['iife'] },
21
+ // },
22
+ // });