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
@@ -25,10 +25,21 @@ function buildFilteredQuery(repo, filters, options = {}) {
25
25
  }
26
26
 
27
27
  for (const [column, filter] of Object.entries(filters)) {
28
- if (!filter || (filter.value === '' && !filter.from && !filter.to)) continue;
29
-
28
+ if (!filter) continue;
29
+
30
30
  const op = filter.op || filter.operator || 'contains';
31
-
31
+
32
+ if (op === 'is_null') {
33
+ query = query.whereNull(column);
34
+ continue;
35
+ }
36
+ if (op === 'is_not_null') {
37
+ query = query.whereNotNull(column);
38
+ continue;
39
+ }
40
+
41
+ if (filter.value === '' && !filter.from && !filter.to) continue;
42
+
32
43
  switch (op) {
33
44
  case 'contains':
34
45
  query = query.where(column, 'like', `%${filter.value}%`);
@@ -86,6 +97,60 @@ async function getAllMatchingIds(repo, filters, primaryKey = 'id', options = {})
86
97
  return records.map(r => r[primaryKey]);
87
98
  }
88
99
 
100
+ const BULK_TEMPORAL_TYPES = new Set(['date', 'datetime', 'timestamp']);
101
+
102
+ /**
103
+ * Bulk field update: coerce date / datetime / timestamp body values to Date (or null).
104
+ * @param {object} columnMeta - ORM column metadata
105
+ * @param {unknown} raw
106
+ * @param {string} fieldName
107
+ * @returns {{ value: Date|null }|{ error: string }}
108
+ */
109
+ function coerceBulkTemporalValue(columnMeta, raw, fieldName) {
110
+ const nullable = !!columnMeta.nullable;
111
+ const empty =
112
+ raw === null ||
113
+ raw === undefined ||
114
+ (typeof raw === 'string' && raw.trim() === '');
115
+
116
+ if (empty) {
117
+ if (nullable) return { value: null };
118
+ return { error: `Field "${fieldName}" requires a value` };
119
+ }
120
+
121
+ if (columnMeta.type === 'date') {
122
+ if (typeof raw !== 'string') return { error: `Invalid date value for "${fieldName}"` };
123
+ const s = raw.trim();
124
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) {
125
+ return { error: `Date must be YYYY-MM-DD for "${fieldName}"` };
126
+ }
127
+ const d = new Date(`${s}T12:00:00.000Z`);
128
+ if (Number.isNaN(d.getTime())) return { error: `Invalid date for "${fieldName}"` };
129
+ return { value: d };
130
+ }
131
+
132
+ if (columnMeta.type === 'datetime' || columnMeta.type === 'timestamp') {
133
+ if (typeof raw !== 'string') return { error: `Invalid datetime value for "${fieldName}"` };
134
+ const s = raw.trim();
135
+ let d;
136
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(s)) {
137
+ d = new Date(s);
138
+ } else if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(s)) {
139
+ d = new Date(s);
140
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
141
+ d = new Date(`${s}T00:00:00`);
142
+ } else {
143
+ d = new Date(s);
144
+ }
145
+ if (Number.isNaN(d.getTime())) {
146
+ return { error: `Invalid datetime for "${fieldName}"` };
147
+ }
148
+ return { value: d };
149
+ }
150
+
151
+ return { error: `Field "${fieldName}" is not a temporal type` };
152
+ }
153
+
89
154
  /**
90
155
  * Create extension API handlers
91
156
  * @param {Object} options - Options
@@ -258,7 +323,7 @@ function createExtensionApiHandlers(options) {
258
323
  }
259
324
 
260
325
  /**
261
- * Bulk update field values (for enum/boolean fields)
326
+ * Bulk update field values (enum, boolean, date, datetime, timestamp)
262
327
  * Supports both specific IDs and selectAll mode with filters
263
328
  */
264
329
  async function bulkUpdateFieldHandler(req, res) {
@@ -283,13 +348,18 @@ function createExtensionApiHandlers(options) {
283
348
  return res.status(400).json({ error: `Field "${field}" not found in model` });
284
349
  }
285
350
 
286
- // Validate field type - only allow enum and boolean
287
351
  const enumValues = columnMeta.enumValues || columnMeta.enum;
288
352
  const isEnum = enumValues && Array.isArray(enumValues);
289
353
  const isBoolean = columnMeta.type === 'boolean';
290
-
291
- if (!isEnum && !isBoolean) {
292
- return res.status(400).json({ error: `Field "${field}" is not an enum or boolean type` });
354
+ const isTemporal =
355
+ BULK_TEMPORAL_TYPES.has(columnMeta.type) &&
356
+ columnMeta.auto !== 'create' &&
357
+ columnMeta.auto !== 'update';
358
+
359
+ if (!isEnum && !isBoolean && !isTemporal) {
360
+ return res.status(400).json({
361
+ error: `Field "${field}" is not bulk-updatable (allowed: enum, boolean, date, datetime, timestamp)`,
362
+ });
293
363
  }
294
364
 
295
365
  // Validate value for enum fields
@@ -297,10 +367,15 @@ function createExtensionApiHandlers(options) {
297
367
  return res.status(400).json({ error: `Invalid value "${value}" for enum field "${field}"` });
298
368
  }
299
369
 
300
- // Coerce boolean value
301
370
  let updateValue = value;
302
371
  if (isBoolean) {
303
372
  updateValue = value === true || value === 'true' || value === 1 || value === '1';
373
+ } else if (isTemporal) {
374
+ const coerced = coerceBulkTemporalValue(columnMeta, value, field);
375
+ if (coerced.error) {
376
+ return res.status(400).json({ error: coerced.error });
377
+ }
378
+ updateValue = coerced.value;
304
379
  }
305
380
 
306
381
  // Get repository and determine IDs
@@ -351,7 +426,7 @@ function createExtensionApiHandlers(options) {
351
426
  }
352
427
 
353
428
  /**
354
- * Get bulk-updatable fields for a model (enum and boolean fields)
429
+ * Get bulk-updatable fields for a model (enum, boolean, date, datetime, timestamp)
355
430
  */
356
431
  function bulkFieldsHandler(req, res) {
357
432
  try {
@@ -364,14 +439,21 @@ function createExtensionApiHandlers(options) {
364
439
  return res.status(404).json({ error: 'Model not found or not enabled' });
365
440
  }
366
441
 
442
+ const hiddenSet = new Set(model.hidden || []);
367
443
  const bulkFields = [];
368
444
 
369
445
  // model.columns is a Map<string, ColumnMeta> - values are already metadata objects
370
446
  for (const [fieldName, columnMeta] of model.columns.entries()) {
447
+ if (hiddenSet.has(fieldName)) continue;
448
+
371
449
  // Check for enum - schema uses 'enumValues' property
372
450
  const enumValues = columnMeta.enumValues || columnMeta.enum;
373
451
  const isEnum = enumValues && Array.isArray(enumValues);
374
452
  const isBoolean = columnMeta.type === 'boolean';
453
+ const isTemporal =
454
+ BULK_TEMPORAL_TYPES.has(columnMeta.type) &&
455
+ columnMeta.auto !== 'create' &&
456
+ columnMeta.auto !== 'update';
375
457
 
376
458
  if (isEnum) {
377
459
  bulkFields.push({
@@ -390,6 +472,13 @@ function createExtensionApiHandlers(options) {
390
472
  { value: false, label: 'False' },
391
473
  ],
392
474
  });
475
+ } else if (isTemporal) {
476
+ bulkFields.push({
477
+ name: fieldName,
478
+ type: columnMeta.type,
479
+ label: columnMeta.label || fieldName,
480
+ nullable: !!columnMeta.nullable,
481
+ });
393
482
  }
394
483
  }
395
484
 
@@ -660,4 +749,5 @@ module.exports = {
660
749
  createExtensionApiHandlers,
661
750
  buildFilteredQuery,
662
751
  getAllMatchingIds,
752
+ coerceBulkTemporalValue,
663
753
  };
@@ -31,6 +31,7 @@ const { registerModule } = require('./core/admin-module');
31
31
  * @param {Object} [options.userManagement.fields] - Field mappings
32
32
  * @param {Function} [options.configure] - Configuration callback (registry) => void
33
33
  * @param {string} [options.uploadUrl] - POST URL for file uploads (overrides app.get('webspresso.uploadPath') from uploadPlugin)
34
+ * @param {boolean} [options.richTextSanitize=true] - Sanitize HTML for admin rich-text fields on create/update (set false only if you accept XSS risk)
34
35
  * @returns {Object} Plugin definition
35
36
  */
36
37
  function adminPanelPlugin(options = {}) {
@@ -43,6 +44,7 @@ function adminPanelPlugin(options = {}) {
43
44
  userManagement: userMgmtConfig,
44
45
  configure,
45
46
  uploadUrl: uploadUrlOption,
47
+ richTextSanitize = true,
46
48
  } = options;
47
49
 
48
50
  // Validate required options
@@ -204,6 +206,7 @@ function adminPanelPlugin(options = {}) {
204
206
  AdminUser,
205
207
  hashPassword: (password, rounds) => bcrypt.hash(password, rounds),
206
208
  comparePassword: (password, hash) => bcrypt.compare(password, hash),
209
+ richTextSanitize,
207
210
  });
208
211
 
209
212
  const extensionHandlers = createExtensionApiHandlers({
@@ -0,0 +1,23 @@
1
+ /** Strip simple HTML-ish tags repeatedly (mitigates incomplete single-pass sanitization). */
2
+ function stripHtmlLikeTagsRepeated(value) {
3
+ let s = String(value);
4
+ let prev;
5
+ do {
6
+ prev = s;
7
+ s = s.replace(/<[^>]*>/g, '');
8
+ } while (s !== prev);
9
+ return s;
10
+ }
11
+
12
+ /**
13
+ * Whether rich-text content is empty after removing tags / Quill placeholders.
14
+ * @param {string} value
15
+ */
16
+ function isRichTextEmpty(value) {
17
+ if (!value) return true;
18
+ const stripped = stripHtmlLikeTagsRepeated(String(value)).trim();
19
+ const v = String(value).trim();
20
+ return stripped === '' || v === '<p><br></p>' || v === '<p></p>';
21
+ }
22
+
23
+ module.exports = { isRichTextEmpty, stripHtmlLikeTagsRepeated };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Server-side HTML sanitization for admin panel Quill/rich-text fields.
3
+ * Intended for XSS mitigation before persistence — not a substitute for safe templating on public pages.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const sanitizeHtml = require('sanitize-html');
9
+
10
+ const RICH_TEXT_SANITIZE_OPTIONS = {
11
+ allowedTags: [
12
+ 'p',
13
+ 'br',
14
+ 'strong',
15
+ 'b',
16
+ 'em',
17
+ 'i',
18
+ 'u',
19
+ 's',
20
+ 'strike',
21
+ 'del',
22
+ 'h1',
23
+ 'h2',
24
+ 'h3',
25
+ 'h4',
26
+ 'h5',
27
+ 'h6',
28
+ 'blockquote',
29
+ 'ul',
30
+ 'ol',
31
+ 'li',
32
+ 'a',
33
+ 'span',
34
+ 'pre',
35
+ 'code',
36
+ ],
37
+ allowedAttributes: {
38
+ a: ['href', 'target', 'rel'],
39
+ span: ['class'],
40
+ p: ['class'],
41
+ ol: ['class'],
42
+ ul: ['class'],
43
+ li: ['class'],
44
+ blockquote: ['class'],
45
+ pre: ['class'],
46
+ code: ['class'],
47
+ h1: ['class'],
48
+ h2: ['class'],
49
+ h3: ['class'],
50
+ h4: ['class'],
51
+ h5: ['class'],
52
+ h6: ['class'],
53
+ },
54
+ allowedClasses: {
55
+ span: [/^ql-/],
56
+ p: [/^ql-/],
57
+ ol: [/^ql-/],
58
+ ul: [/^ql-/],
59
+ li: [/^ql-/],
60
+ blockquote: [/^ql-/],
61
+ pre: [/^ql-/],
62
+ code: [/^language-/, /^lang-/, /^ql-/],
63
+ h1: [/^ql-/],
64
+ h2: [/^ql-/],
65
+ h3: [/^ql-/],
66
+ h4: [/^ql-/],
67
+ h5: [/^ql-/],
68
+ h6: [/^ql-/],
69
+ },
70
+ allowedSchemesByTag: {
71
+ a: ['http', 'https', 'mailto', 'tel'],
72
+ },
73
+ allowedSchemes: ['http', 'https', 'mailto', 'tel'],
74
+ allowProtocolRelative: true,
75
+ transformTags: {
76
+ a(tagName, attribs) {
77
+ const href = (attribs.href || '').trim();
78
+ if (/^javascript:/i.test(href) || /^vbscript:/i.test(href) || /^data:/i.test(href)) {
79
+ delete attribs.href;
80
+ }
81
+ if (attribs.target === '_blank') {
82
+ attribs.rel = 'noopener noreferrer';
83
+ }
84
+ return { tagName, attribs };
85
+ },
86
+ },
87
+ };
88
+
89
+ /**
90
+ * @param {unknown} input
91
+ * @returns {unknown}
92
+ */
93
+ function sanitizeRichHtml(input) {
94
+ if (input === null || input === undefined) {
95
+ return input;
96
+ }
97
+ if (typeof input !== 'string') {
98
+ return input;
99
+ }
100
+ return sanitizeHtml(input, RICH_TEXT_SANITIZE_OPTIONS);
101
+ }
102
+
103
+ module.exports = {
104
+ sanitizeRichHtml,
105
+ RICH_TEXT_SANITIZE_OPTIONS,
106
+ };
@@ -277,17 +277,18 @@ const WidgetRenderers = {
277
277
  if (data.error) {
278
278
  return m('div.text-sm.text-red-600.dark:text-red-400', data.error);
279
279
  }
280
+ const stat = (v) => (v == null ? '—' : v);
280
281
  return m('div.grid.grid-cols-2.gap-4', [
281
282
  m('div.text-center.p-3.bg-blue-50.dark:bg-blue-950/40.rounded-lg', [
282
283
  m('p.text-2xl.font-bold.text-blue-600.dark:text-blue-400', data.total),
283
284
  m('p.text-xs.text-gray-500.dark:text-slate-400', 'Total Users'),
284
285
  ]),
285
286
  m('div.text-center.p-3.bg-green-50.dark:bg-green-950/40.rounded-lg', [
286
- m('p.text-2xl.font-bold.text-green-600.dark:text-green-400', data.active),
287
+ m('p.text-2xl.font-bold.text-green-600.dark:text-green-400', stat(data.active)),
287
288
  m('p.text-xs.text-gray-500.dark:text-slate-400', 'Active'),
288
289
  ]),
289
290
  m('div.text-center.p-3.bg-yellow-50.dark:bg-amber-950/40.rounded-lg', [
290
- m('p.text-2xl.font-bold.text-yellow-600.dark:text-amber-400', data.admins),
291
+ m('p.text-2xl.font-bold.text-yellow-600.dark:text-amber-400', stat(data.admins)),
291
292
  m('p.text-xs.text-gray-500.dark:text-slate-400', 'Admins'),
292
293
  ]),
293
294
  m('div.text-center.p-3.bg-purple-50.dark:bg-purple-950/40.rounded-lg', [
@@ -18,6 +18,29 @@ function truthyBooleanForDb(db) {
18
18
  return true;
19
19
  }
20
20
 
21
+ /**
22
+ * True when the physical table has the column (handles schema vs migration drift).
23
+ */
24
+ async function physicalColumnExists(db, table, column) {
25
+ if (!db?.knex?.schema || !table || !column) return false;
26
+ try {
27
+ return await db.knex.schema.hasColumn(table, column);
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /** `userManagement: { model: 'Member' }` is the public API; `modelName` is also accepted. */
34
+ function resolveUserManagementModelName(config = {}) {
35
+ if (config.modelName != null && String(config.modelName).trim() !== '') {
36
+ return String(config.modelName).trim();
37
+ }
38
+ if (config.model != null && String(config.model).trim() !== '') {
39
+ return String(config.model).trim();
40
+ }
41
+ return 'User';
42
+ }
43
+
21
44
  /**
22
45
  * Register user management in admin panel
23
46
  * @param {Object} options - Options
@@ -30,12 +53,13 @@ function registerUserManagement(options) {
30
53
  const { registry, db, auth, config = {} } = options;
31
54
 
32
55
  const {
33
- modelName = 'User',
34
56
  fields = {},
35
57
  roles = ['user', 'admin'],
36
58
  passwordMinLength = 8,
37
59
  } = config;
38
60
 
61
+ const modelName = resolveUserManagementModelName(config);
62
+
39
63
  const fieldMap = {
40
64
  email: 'email',
41
65
  password: 'password',
@@ -104,14 +128,34 @@ function registerUserManagement(options) {
104
128
  try {
105
129
  const repo = db.getRepository(modelName);
106
130
  const model = repo.model;
131
+ const cols = model.columns;
132
+ const table = model.table;
107
133
  const total = await repo.count();
108
- const activeEq = truthyBooleanForDb(db);
109
- const active = await repo.query().where(fieldMap.active, activeEq).count();
110
- const admins = await repo.count({ [fieldMap.role]: 'admin' });
134
+
135
+ const activeCol = fieldMap.active;
136
+ const hasActiveCol =
137
+ cols?.has(activeCol) && (await physicalColumnExists(db, table, activeCol));
138
+ let active = null;
139
+ let inactive = null;
140
+ if (hasActiveCol) {
141
+ const activeEq = truthyBooleanForDb(db);
142
+ active = await repo.query().where(activeCol, activeEq).count();
143
+ inactive = Math.max(0, total - active);
144
+ }
145
+
146
+ const roleCol = fieldMap.role;
147
+ let admins = null;
148
+ const hasRoleCol =
149
+ cols?.has(roleCol) && (await physicalColumnExists(db, table, roleCol));
150
+ if (hasRoleCol) {
151
+ admins = await repo.count({ [roleCol]: 'admin' });
152
+ }
111
153
 
112
154
  let recentUsers = 0;
113
155
  const createdCol = fieldMap.createdAt;
114
- if (model.columns && model.columns.has(createdCol)) {
156
+ const hasCreatedCol =
157
+ cols?.has(createdCol) && (await physicalColumnExists(db, table, createdCol));
158
+ if (hasCreatedCol) {
115
159
  const weekAgo = new Date();
116
160
  weekAgo.setDate(weekAgo.getDate() - 7);
117
161
  recentUsers = await repo.query()
@@ -122,12 +166,19 @@ function registerUserManagement(options) {
122
166
  return {
123
167
  total,
124
168
  active,
125
- inactive: Math.max(0, total - active),
169
+ inactive,
126
170
  admins,
127
171
  recentUsers,
128
172
  };
129
173
  } catch (e) {
130
- return { total: 0, active: 0, inactive: 0, admins: 0, recentUsers: 0, error: e.message };
174
+ return {
175
+ total: 0,
176
+ active: null,
177
+ inactive: null,
178
+ admins: null,
179
+ recentUsers: 0,
180
+ error: e.message,
181
+ };
131
182
  }
132
183
  },
133
184
  });
@@ -255,10 +306,8 @@ function registerUserManagement(options) {
255
306
  */
256
307
  function createUserManagementApiHandlers(options) {
257
308
  const { db, config, auth } = options;
258
- const {
259
- modelName = 'User',
260
- fields = {},
261
- } = config;
309
+ const { fields = {} } = config;
310
+ const modelName = resolveUserManagementModelName(config);
262
311
 
263
312
  const fieldMap = {
264
313
  email: 'email',
@@ -275,6 +324,8 @@ function createUserManagementApiHandlers(options) {
275
324
  async function listUsers(req, res) {
276
325
  try {
277
326
  const repo = db.getRepository(modelName);
327
+ const model = repo.model;
328
+ const table = model.table;
278
329
  const page = parseInt(req.query.page) || 1;
279
330
  const perPage = parseInt(req.query.perPage) || 20;
280
331
  const search = req.query.search;
@@ -283,21 +334,40 @@ function createUserManagementApiHandlers(options) {
283
334
 
284
335
  let query = repo.query();
285
336
 
286
- // Search
337
+ // Search (only columns that exist on the physical table)
287
338
  if (search) {
288
- query = query.where(function() {
289
- this.where(fieldMap.email, 'like', `%${search}%`)
290
- .orWhere(fieldMap.name, 'like', `%${search}%`);
291
- });
339
+ const hasEmail = await physicalColumnExists(db, table, fieldMap.email);
340
+ const hasName = await physicalColumnExists(db, table, fieldMap.name);
341
+ if (hasEmail || hasName) {
342
+ query = query.where(function () {
343
+ if (hasEmail && hasName) {
344
+ this.where(fieldMap.email, 'like', `%${search}%`).orWhere(
345
+ fieldMap.name,
346
+ 'like',
347
+ `%${search}%`
348
+ );
349
+ } else if (hasEmail) {
350
+ this.where(fieldMap.email, 'like', `%${search}%`);
351
+ } else {
352
+ this.where(fieldMap.name, 'like', `%${search}%`);
353
+ }
354
+ });
355
+ }
292
356
  }
293
357
 
294
- // Role filter
295
- if (role) {
358
+ if (
359
+ role &&
360
+ model.columns?.has(fieldMap.role) &&
361
+ (await physicalColumnExists(db, table, fieldMap.role))
362
+ ) {
296
363
  query = query.where(fieldMap.role, role);
297
364
  }
298
365
 
299
- // Active filter
300
- if (active !== undefined) {
366
+ if (
367
+ active !== undefined &&
368
+ model.columns?.has(fieldMap.active) &&
369
+ (await physicalColumnExists(db, table, fieldMap.active))
370
+ ) {
301
371
  query = query.where(fieldMap.active, active === 'true');
302
372
  }
303
373
 
package/plugins/index.js CHANGED
@@ -19,6 +19,8 @@ const ormCacheAdminPlugin = require('./orm-cache-admin');
19
19
  const { uploadPlugin, createLocalFileProvider } = require('./upload');
20
20
  /** Register after adminPanelPlugin (same db + adminPath) for session and routes. */
21
21
  const { dataExchangePlugin } = require('./data-exchange');
22
+ const { redirectPlugin } = require('./redirect');
23
+ const { rateLimitPlugin } = require('./rate-limit');
22
24
 
23
25
  module.exports = {
24
26
  sitemapPlugin,
@@ -37,5 +39,7 @@ module.exports = {
37
39
  uploadPlugin,
38
40
  createLocalFileProvider,
39
41
  dataExchangePlugin,
42
+ redirectPlugin,
43
+ rateLimitPlugin,
40
44
  };
41
45