webspresso 0.0.100 → 0.0.102
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/.agents/ADMIN.md +47 -4
- package/.agents/AGENTS.md +3 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/AUTH.md +63 -0
- package/.agents/CLI.md +81 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +59 -0
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +163 -3
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +7 -0
- package/README.md +18 -5
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/routes.js +8 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +17 -0
- package/core/applySchema.js +18 -1
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +10 -0
- package/core/auth/middleware.js +15 -1
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +6 -0
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +4 -0
- package/index.d.ts +422 -1
- package/index.js +111 -16
- package/package.json +9 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +29 -0
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +1 -1
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/index.js +13 -0
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/plugins/redirect/index.js +1 -1
- package/src/api/api-loader.js +18 -2
- package/src/api/validation-i18n.js +231 -0
- package/src/app-context.js +48 -6
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +80 -5
- package/src/file-router.js +153 -91
- package/src/helpers.js +143 -0
- package/src/njk-frontmatter.js +17 -1
- package/src/pages/page-loader.js +1 -1
- package/src/plugin-manager.js +12 -0
- package/src/routing/cascading.js +14 -10
- package/src/routing/route-table.js +1 -1
- package/src/server.js +243 -34
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- package/src/template/precompile.js +247 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso - Flash Messages & Old Input Manager (AdonisJS & Django-inspired)
|
|
3
|
+
* Provides session-backed or cookie-backed one-time flash storage across redirects
|
|
4
|
+
* @module core/forms/flash
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const FLASH_SESSION_KEY = '__ws_flash';
|
|
8
|
+
const FLASH_COOKIE_NAME = '__ws_flash';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format and normalize errors into a standard `{ fieldName: string[] }` shape
|
|
12
|
+
* Supports ZodError, plain object, array, or string error.
|
|
13
|
+
* @param {any} errors
|
|
14
|
+
* @returns {Record<string, string[]>}
|
|
15
|
+
*/
|
|
16
|
+
function normalizeErrors(errors) {
|
|
17
|
+
if (!errors) return {};
|
|
18
|
+
|
|
19
|
+
// 1. ZodError with format() or flatten() or issues
|
|
20
|
+
if (errors.issues && Array.isArray(errors.issues)) {
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const issue of errors.issues) {
|
|
23
|
+
const field = issue.path && issue.path.length > 0 ? issue.path.join('.') : '_form';
|
|
24
|
+
if (!result[field]) result[field] = [];
|
|
25
|
+
result[field].push(issue.message || 'Invalid value');
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof errors.flatten === 'function') {
|
|
31
|
+
const flat = errors.flatten();
|
|
32
|
+
const result = { ...(flat.fieldErrors || {}) };
|
|
33
|
+
if (flat.formErrors && flat.formErrors.length > 0) {
|
|
34
|
+
result._form = flat.formErrors;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Simple string error
|
|
40
|
+
if (typeof errors === 'string') {
|
|
41
|
+
return { _form: [errors] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Array of strings or objects
|
|
45
|
+
if (Array.isArray(errors)) {
|
|
46
|
+
return {
|
|
47
|
+
_form: errors.map((e) => (typeof e === 'string' ? e : e.message || String(e))),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 4. Plain object `{ field: string | string[] }`
|
|
52
|
+
if (typeof errors === 'object') {
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const [key, val] of Object.entries(errors)) {
|
|
55
|
+
if (Array.isArray(val)) {
|
|
56
|
+
result[key] = val.map((v) => (typeof v === 'string' ? v : v.message || String(v)));
|
|
57
|
+
} else if (typeof val === 'string') {
|
|
58
|
+
result[key] = [val];
|
|
59
|
+
} else if (val && typeof val === 'object' && val.message) {
|
|
60
|
+
result[key] = [String(val.message)];
|
|
61
|
+
} else if (val !== null && val !== undefined) {
|
|
62
|
+
result[key] = [String(val)];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { _form: [String(errors)] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Encode flash data for cookie transport (base64url JSON)
|
|
73
|
+
* @param {Object} data
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function encodeFlashCookie(data) {
|
|
77
|
+
try {
|
|
78
|
+
return Buffer.from(JSON.stringify(data), 'utf8').toString('base64url');
|
|
79
|
+
} catch {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decode flash data from cookie
|
|
86
|
+
* @param {string} raw
|
|
87
|
+
* @returns {Object|null}
|
|
88
|
+
*/
|
|
89
|
+
function decodeFlashCookie(raw) {
|
|
90
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
91
|
+
try {
|
|
92
|
+
const json = Buffer.from(raw, 'base64url').toString('utf8');
|
|
93
|
+
return JSON.parse(json);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Flash and Old Input Express Middleware
|
|
101
|
+
* @param {Object} [options={}]
|
|
102
|
+
* @returns {import('express').RequestHandler}
|
|
103
|
+
*/
|
|
104
|
+
function createFlashMiddleware(options = {}) {
|
|
105
|
+
const cookieName = options.cookieName || FLASH_COOKIE_NAME;
|
|
106
|
+
|
|
107
|
+
return function flashMiddleware(req, res, next) {
|
|
108
|
+
// 1. Load incoming flash messages from session or cookie
|
|
109
|
+
let incomingFlash = {};
|
|
110
|
+
|
|
111
|
+
if (req.session && req.session[FLASH_SESSION_KEY]) {
|
|
112
|
+
incomingFlash = req.session[FLASH_SESSION_KEY] || {};
|
|
113
|
+
// Delete from session for next request (consume)
|
|
114
|
+
delete req.session[FLASH_SESSION_KEY];
|
|
115
|
+
} else if (req.cookies && req.cookies[cookieName]) {
|
|
116
|
+
incomingFlash = decodeFlashCookie(req.cookies[cookieName]) || {};
|
|
117
|
+
// Clear cookie immediately
|
|
118
|
+
res.clearCookie(cookieName, { path: '/' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Outgoing flash state to be persisted for the NEXT request
|
|
122
|
+
const outgoingFlash = {};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Set or get a flash message
|
|
126
|
+
* @param {string} [key]
|
|
127
|
+
* @param {any} [value]
|
|
128
|
+
* @returns {any}
|
|
129
|
+
*/
|
|
130
|
+
req.flash = function (key, value) {
|
|
131
|
+
if (arguments.length === 0) {
|
|
132
|
+
return incomingFlash;
|
|
133
|
+
}
|
|
134
|
+
if (arguments.length === 1) {
|
|
135
|
+
return incomingFlash[key];
|
|
136
|
+
}
|
|
137
|
+
outgoingFlash[key] = value;
|
|
138
|
+
saveOutgoingFlash();
|
|
139
|
+
return value;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get old input value from previous request
|
|
144
|
+
* @param {string} [name]
|
|
145
|
+
* @param {any} [defaultValue=null]
|
|
146
|
+
* @returns {any}
|
|
147
|
+
*/
|
|
148
|
+
req.old = function (name, defaultValue = null) {
|
|
149
|
+
const oldInputs = incomingFlash.old || {};
|
|
150
|
+
if (!name) return oldInputs;
|
|
151
|
+
return oldInputs[name] !== undefined ? oldInputs[name] : defaultValue;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Flash current request body / input for the next request
|
|
156
|
+
* @param {Object} [customInput]
|
|
157
|
+
* @returns {res}
|
|
158
|
+
*/
|
|
159
|
+
res.withInput = function (customInput) {
|
|
160
|
+
const input = customInput || req.body || {};
|
|
161
|
+
// Exclude sensitive fields by default
|
|
162
|
+
const sanitized = { ...input };
|
|
163
|
+
delete sanitized.password;
|
|
164
|
+
delete sanitized.password_confirmation;
|
|
165
|
+
delete sanitized.confirmPassword;
|
|
166
|
+
delete sanitized._csrf;
|
|
167
|
+
|
|
168
|
+
outgoingFlash.old = sanitized;
|
|
169
|
+
saveOutgoingFlash();
|
|
170
|
+
return res;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Flash validation errors for the next request
|
|
175
|
+
* @param {any} errors - ZodError, object, array, or string
|
|
176
|
+
* @returns {res}
|
|
177
|
+
*/
|
|
178
|
+
res.withErrors = function (errors) {
|
|
179
|
+
outgoingFlash.errors = normalizeErrors(errors);
|
|
180
|
+
saveOutgoingFlash();
|
|
181
|
+
return res;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Flash a key-value message for the next request
|
|
186
|
+
* @param {string} key - e.g. 'success', 'warning', 'info'
|
|
187
|
+
* @param {any} message
|
|
188
|
+
* @returns {res}
|
|
189
|
+
*/
|
|
190
|
+
res.withFlash = function (key, message) {
|
|
191
|
+
outgoingFlash[key] = message;
|
|
192
|
+
saveOutgoingFlash();
|
|
193
|
+
return res;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
function saveOutgoingFlash() {
|
|
197
|
+
if (req.session) {
|
|
198
|
+
req.session[FLASH_SESSION_KEY] = outgoingFlash;
|
|
199
|
+
} else {
|
|
200
|
+
// Remove any previously set cookie with the same name from response headers
|
|
201
|
+
const existing = res.getHeader('Set-Cookie');
|
|
202
|
+
if (existing) {
|
|
203
|
+
const arr = Array.isArray(existing) ? existing : [existing];
|
|
204
|
+
const filtered = arr.filter((c) => !c.startsWith(`${cookieName}=`));
|
|
205
|
+
if (filtered.length > 0) {
|
|
206
|
+
res.setHeader('Set-Cookie', filtered);
|
|
207
|
+
} else {
|
|
208
|
+
res.removeHeader('Set-Cookie');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const encoded = encodeFlashCookie(outgoingFlash);
|
|
212
|
+
res.cookie(cookieName, encoded, {
|
|
213
|
+
path: '/',
|
|
214
|
+
httpOnly: true,
|
|
215
|
+
sameSite: 'lax',
|
|
216
|
+
maxAge: 300000, // 5 minutes
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Attach to request context for Nunjucks helpers
|
|
222
|
+
req.__flash = incomingFlash;
|
|
223
|
+
req.__outgoingFlash = outgoingFlash;
|
|
224
|
+
|
|
225
|
+
next();
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
createFlashMiddleware,
|
|
231
|
+
normalizeErrors,
|
|
232
|
+
FLASH_SESSION_KEY,
|
|
233
|
+
FLASH_COOKIE_NAME,
|
|
234
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso - Form Builder & HTML Generator (Django ModelForm-inspired)
|
|
3
|
+
* Generates accessible HTML form inputs, field wrappers, and auto-forms from ORM models
|
|
4
|
+
* @module core/forms/form-builder
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function escapeHtml(str) {
|
|
8
|
+
if (str === null || str === undefined) return '';
|
|
9
|
+
return String(str)
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render a single form field with label, input, and validation error message
|
|
19
|
+
* @param {string} name - Field name
|
|
20
|
+
* @param {Object} [options={}] - Field configuration options
|
|
21
|
+
* @param {Object} [ctx={}] - Request / helpers context
|
|
22
|
+
* @returns {string} HTML string
|
|
23
|
+
*/
|
|
24
|
+
function renderField(name, options = {}, ctx = {}) {
|
|
25
|
+
const {
|
|
26
|
+
label = humanize(name),
|
|
27
|
+
type = 'text',
|
|
28
|
+
placeholder = '',
|
|
29
|
+
required = false,
|
|
30
|
+
disabled = false,
|
|
31
|
+
readonly = false,
|
|
32
|
+
className = 'form-control',
|
|
33
|
+
wrapperClass = 'form-group mb-3',
|
|
34
|
+
errorClass = 'is-invalid',
|
|
35
|
+
showError = true,
|
|
36
|
+
rows = 4,
|
|
37
|
+
selectOptions = [],
|
|
38
|
+
helpText = '',
|
|
39
|
+
attributes = {},
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
// Resolve old value or default
|
|
43
|
+
const req = ctx.req;
|
|
44
|
+
const oldVal = req && typeof req.old === 'function' ? req.old(name) : undefined;
|
|
45
|
+
const value = oldVal !== undefined ? oldVal : (options.value !== undefined ? options.value : (options.default ?? ''));
|
|
46
|
+
|
|
47
|
+
// Check error state
|
|
48
|
+
const errors = req?.__flash?.errors || {};
|
|
49
|
+
const fieldErrors = errors[name] || [];
|
|
50
|
+
const hasError = fieldErrors.length > 0;
|
|
51
|
+
const inputClass = `${className}${hasError ? ` ${errorClass}` : ''}`.trim();
|
|
52
|
+
|
|
53
|
+
const extraAttrs = Object.entries(attributes)
|
|
54
|
+
.map(([k, v]) => `${escapeHtml(k)}="${escapeHtml(v)}"`)
|
|
55
|
+
.join(' ');
|
|
56
|
+
|
|
57
|
+
let inputHtml = '';
|
|
58
|
+
|
|
59
|
+
if (type === 'textarea') {
|
|
60
|
+
inputHtml = `<textarea id="field_${escapeHtml(name)}" name="${escapeHtml(name)}" class="${escapeHtml(inputClass)}" placeholder="${escapeHtml(placeholder)}"${required ? ' required' : ''}${disabled ? ' disabled' : ''}${readonly ? ' readonly' : ''} rows="${rows}"${extraAttrs ? ' ' + extraAttrs : ''}>${escapeHtml(value)}</textarea>`;
|
|
61
|
+
} else if (type === 'select') {
|
|
62
|
+
const optsHtml = selectOptions
|
|
63
|
+
.map((opt) => {
|
|
64
|
+
const optVal = typeof opt === 'object' && opt !== null ? opt.value : opt;
|
|
65
|
+
const optLabel = typeof opt === 'object' && opt !== null ? opt.label : opt;
|
|
66
|
+
const isSelected = String(optVal) === String(value);
|
|
67
|
+
return `<option value="${escapeHtml(optVal)}"${isSelected ? ' selected' : ''}>${escapeHtml(optLabel)}</option>`;
|
|
68
|
+
})
|
|
69
|
+
.join('\n ');
|
|
70
|
+
inputHtml = `<select id="field_${escapeHtml(name)}" name="${escapeHtml(name)}" class="${escapeHtml(inputClass)}"${required ? ' required' : ''}${disabled ? ' disabled' : ''}${extraAttrs ? ' ' + extraAttrs : ''}>\n ${optsHtml}\n </select>`;
|
|
71
|
+
} else if (type === 'checkbox') {
|
|
72
|
+
const isChecked = Boolean(value) && value !== '0' && value !== 'false';
|
|
73
|
+
inputHtml = `<div class="form-check">
|
|
74
|
+
<input type="checkbox" id="field_${escapeHtml(name)}" name="${escapeHtml(name)}" value="true" class="form-check-input${hasError ? ` ${errorClass}` : ''}"${isChecked ? ' checked' : ''}${disabled ? ' disabled' : ''}${extraAttrs ? ' ' + extraAttrs : ''}>
|
|
75
|
+
<label class="form-check-label" for="field_${escapeHtml(name)}">${escapeHtml(label)}</label>
|
|
76
|
+
</div>`;
|
|
77
|
+
} else {
|
|
78
|
+
// Standard input (text, email, password, number, date, file, etc.)
|
|
79
|
+
inputHtml = `<input type="${escapeHtml(type)}" id="field_${escapeHtml(name)}" name="${escapeHtml(name)}" value="${escapeHtml(value)}" class="${escapeHtml(inputClass)}" placeholder="${escapeHtml(placeholder)}"${required ? ' required' : ''}${disabled ? ' disabled' : ''}${readonly ? ' readonly' : ''}${extraAttrs ? ' ' + extraAttrs : ''}>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const labelHtml =
|
|
83
|
+
type !== 'checkbox' && label
|
|
84
|
+
? `<label for="field_${escapeHtml(name)}" class="form-label">${escapeHtml(label)}${required ? ' <span class="text-danger">*</span>' : ''}</label>`
|
|
85
|
+
: '';
|
|
86
|
+
|
|
87
|
+
const helpHtml = helpText ? `<div class="form-text text-muted">${escapeHtml(helpText)}</div>` : '';
|
|
88
|
+
|
|
89
|
+
const errorHtml =
|
|
90
|
+
showError && hasError
|
|
91
|
+
? `<div class="invalid-feedback d-block text-danger mt-1">${escapeHtml(fieldErrors[0])}</div>`
|
|
92
|
+
: '';
|
|
93
|
+
|
|
94
|
+
return `<div class="${escapeHtml(wrapperClass)}">
|
|
95
|
+
${labelHtml ? labelHtml + '\n ' : ''}${inputHtml}
|
|
96
|
+
${helpHtml ? helpHtml + '\n ' : ''}${errorHtml}
|
|
97
|
+
</div>`.trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate a complete HTML form from an ORM model definition (Django ModelForm-inspired)
|
|
102
|
+
* @param {Object} modelOrRepo - ORM model or repository
|
|
103
|
+
* @param {Object} [options={}] - Form options (action, method, exclude, include, submitLabel)
|
|
104
|
+
* @param {Object} [ctx={}] - Request / helpers context
|
|
105
|
+
* @returns {string} HTML string
|
|
106
|
+
*/
|
|
107
|
+
function renderFormFor(modelOrRepo, options = {}, ctx = {}) {
|
|
108
|
+
const model = modelOrRepo.model || modelOrRepo;
|
|
109
|
+
if (!model || !model.columns) {
|
|
110
|
+
throw new Error('renderFormFor requires an ORM model or repository with column definitions');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const {
|
|
114
|
+
action = '',
|
|
115
|
+
method = 'POST',
|
|
116
|
+
exclude = ['id', 'created_at', 'updated_at', 'deleted_at'],
|
|
117
|
+
include = null,
|
|
118
|
+
submitLabel = 'Save',
|
|
119
|
+
csrf = true,
|
|
120
|
+
formClass = 'webspresso-form',
|
|
121
|
+
} = options;
|
|
122
|
+
|
|
123
|
+
const excludeSet = new Set(exclude);
|
|
124
|
+
const includeSet = include ? new Set(include) : null;
|
|
125
|
+
|
|
126
|
+
const fieldsHtml = [];
|
|
127
|
+
|
|
128
|
+
for (const [colName, meta] of model.columns.entries()) {
|
|
129
|
+
if (excludeSet.has(colName)) continue;
|
|
130
|
+
if (includeSet && !includeSet.has(colName)) continue;
|
|
131
|
+
|
|
132
|
+
const fieldConfig = inferFieldConfig(colName, meta);
|
|
133
|
+
fieldsHtml.push(renderField(colName, fieldConfig, ctx));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let csrfHtml = '';
|
|
137
|
+
if (csrf && ctx.req) {
|
|
138
|
+
const token = ctx.req.csrfToken ? ctx.req.csrfToken() : ctx.req.session?._csrf || '';
|
|
139
|
+
if (token) {
|
|
140
|
+
csrfHtml = ` <input type="hidden" name="_csrf" value="${escapeHtml(token)}">\n`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const submitHtml = submitLabel
|
|
145
|
+
? ` <div class="form-actions mt-4">\n <button type="submit" class="btn btn-primary">${escapeHtml(submitLabel)}</button>\n </div>`
|
|
146
|
+
: '';
|
|
147
|
+
|
|
148
|
+
return `<form action="${escapeHtml(action)}" method="${escapeHtml(method)}" class="${escapeHtml(formClass)}">
|
|
149
|
+
${csrfHtml} ${fieldsHtml.join('\n ')}
|
|
150
|
+
${submitHtml}
|
|
151
|
+
</form>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function inferFieldConfig(name, meta) {
|
|
155
|
+
const config = {
|
|
156
|
+
label: humanize(name),
|
|
157
|
+
required: !meta.nullable && meta.default === undefined && !meta.auto,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
switch (meta.type) {
|
|
161
|
+
case 'text':
|
|
162
|
+
config.type = 'textarea';
|
|
163
|
+
break;
|
|
164
|
+
case 'boolean':
|
|
165
|
+
config.type = 'checkbox';
|
|
166
|
+
break;
|
|
167
|
+
case 'integer':
|
|
168
|
+
case 'bigint':
|
|
169
|
+
case 'float':
|
|
170
|
+
case 'decimal':
|
|
171
|
+
config.type = 'number';
|
|
172
|
+
break;
|
|
173
|
+
case 'date':
|
|
174
|
+
config.type = 'date';
|
|
175
|
+
break;
|
|
176
|
+
case 'datetime':
|
|
177
|
+
case 'timestamp':
|
|
178
|
+
config.type = 'datetime-local';
|
|
179
|
+
break;
|
|
180
|
+
case 'file':
|
|
181
|
+
case 'files':
|
|
182
|
+
config.type = 'file';
|
|
183
|
+
break;
|
|
184
|
+
case 'enum':
|
|
185
|
+
config.type = 'select';
|
|
186
|
+
config.selectOptions = (meta.enumValues || []).map((v) => ({ label: humanize(v), value: v }));
|
|
187
|
+
break;
|
|
188
|
+
default:
|
|
189
|
+
if (name.includes('email')) {
|
|
190
|
+
config.type = 'email';
|
|
191
|
+
} else if (name.includes('password')) {
|
|
192
|
+
config.type = 'password';
|
|
193
|
+
} else {
|
|
194
|
+
config.type = 'text';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return config;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function humanize(str) {
|
|
202
|
+
if (!str) return '';
|
|
203
|
+
return str
|
|
204
|
+
.replace(/[_-]+/g, ' ')
|
|
205
|
+
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
|
|
206
|
+
.replace(/^./, (m) => m.toUpperCase());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
renderField,
|
|
211
|
+
renderFormFor,
|
|
212
|
+
humanize,
|
|
213
|
+
escapeHtml,
|
|
214
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Forms & Flash Subsystem (AdonisJS & Django-inspired)
|
|
3
|
+
* @module core/forms
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { createFlashMiddleware, normalizeErrors } = require('./flash');
|
|
7
|
+
const { renderField, renderFormFor, humanize, escapeHtml } = require('./form-builder');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
createFlashMiddleware,
|
|
11
|
+
normalizeErrors,
|
|
12
|
+
renderField,
|
|
13
|
+
renderFormFor,
|
|
14
|
+
humanize,
|
|
15
|
+
escapeHtml,
|
|
16
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Mail Module
|
|
3
|
+
* @module core/mail
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Mailable } = require('./mailable');
|
|
7
|
+
const { Mailer, createMailer } = require('./mailer');
|
|
8
|
+
const {
|
|
9
|
+
MemoryTransport,
|
|
10
|
+
ConsoleTransport,
|
|
11
|
+
FileTransport,
|
|
12
|
+
resolveTransport,
|
|
13
|
+
} = require('./transports');
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
Mailable,
|
|
17
|
+
Mailer,
|
|
18
|
+
createMailer,
|
|
19
|
+
MemoryTransport,
|
|
20
|
+
ConsoleTransport,
|
|
21
|
+
FileTransport,
|
|
22
|
+
resolveTransport,
|
|
23
|
+
};
|