@beechcms/core 0.4.3 → 0.6.0-preview.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.
Files changed (125) hide show
  1. package/dist/auth/hash-provider.d.ts.map +1 -1
  2. package/dist/auth/hash-provider.js +2 -0
  3. package/dist/auth/password-reset-token.repository.d.ts.map +1 -1
  4. package/dist/auth/password-reset-token.repository.js +2 -0
  5. package/dist/auth/session.repository.d.ts.map +1 -1
  6. package/dist/auth/session.repository.js +2 -0
  7. package/dist/auth/token-service.d.ts +2 -0
  8. package/dist/auth/token-service.d.ts.map +1 -1
  9. package/dist/auth/token-service.js +2 -0
  10. package/dist/auth/user.repository.d.ts +4 -1
  11. package/dist/auth/user.repository.d.ts.map +1 -1
  12. package/dist/auth/user.repository.js +2 -0
  13. package/dist/automations-grammar-words.d.ts.map +1 -1
  14. package/dist/automations-grammar-words.js +2 -0
  15. package/dist/automations.repository.interface.d.ts.map +1 -1
  16. package/dist/automations.repository.interface.js +2 -0
  17. package/dist/automations.runner.interface.d.ts.map +1 -1
  18. package/dist/automations.runner.interface.js +2 -0
  19. package/dist/automations.runner.stub.d.ts.map +1 -1
  20. package/dist/automations.runner.stub.js +2 -0
  21. package/dist/automations.types.d.ts +2 -0
  22. package/dist/automations.types.d.ts.map +1 -1
  23. package/dist/automations.types.js +2 -0
  24. package/dist/clock.d.ts.map +1 -1
  25. package/dist/clock.js +2 -0
  26. package/dist/content-scan.repository.d.ts.map +1 -1
  27. package/dist/content-scan.repository.js +2 -0
  28. package/dist/content.repository.d.ts +62 -0
  29. package/dist/content.repository.d.ts.map +1 -1
  30. package/dist/content.repository.js +18 -0
  31. package/dist/define-seed.d.ts.map +1 -1
  32. package/dist/define-seed.js +2 -0
  33. package/dist/demo-data.repository.d.ts +5 -0
  34. package/dist/demo-data.repository.d.ts.map +1 -0
  35. package/dist/demo-data.repository.js +3 -0
  36. package/dist/engine.d.ts +76 -0
  37. package/dist/engine.d.ts.map +1 -1
  38. package/dist/engine.js +253 -8
  39. package/dist/file-types.d.ts +13 -0
  40. package/dist/file-types.d.ts.map +1 -0
  41. package/dist/file-types.js +85 -0
  42. package/dist/id-generator.d.ts +7 -0
  43. package/dist/id-generator.d.ts.map +1 -1
  44. package/dist/id-generator.js +5 -0
  45. package/dist/idempotency.repository.d.ts.map +1 -1
  46. package/dist/idempotency.repository.js +2 -0
  47. package/dist/index.d.ts +12 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +14 -1
  50. package/dist/layout-permissions.d.ts +3 -0
  51. package/dist/layout-permissions.d.ts.map +1 -0
  52. package/dist/layout-permissions.js +11 -0
  53. package/dist/media.repository.d.ts.map +1 -1
  54. package/dist/media.repository.js +2 -0
  55. package/dist/notifications/notification-service.d.ts.map +1 -1
  56. package/dist/notifications/notification-service.js +2 -0
  57. package/dist/notifications/notification.repository.d.ts.map +1 -1
  58. package/dist/notifications/notification.repository.js +2 -9
  59. package/dist/observability/activity-log.repository.d.ts.map +1 -1
  60. package/dist/observability/activity-log.repository.js +2 -0
  61. package/dist/observability/activity-logger.d.ts +2 -2
  62. package/dist/observability/activity-logger.d.ts.map +1 -1
  63. package/dist/observability/activity-logger.js +2 -11
  64. package/dist/observability/analytics.repository.d.ts.map +1 -1
  65. package/dist/observability/analytics.repository.js +2 -0
  66. package/dist/policies.d.ts.map +1 -1
  67. package/dist/policies.js +2 -0
  68. package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
  69. package/dist/rate-limit/rate-limiter.js +2 -0
  70. package/dist/relations.d.ts +28 -0
  71. package/dist/relations.d.ts.map +1 -0
  72. package/dist/relations.js +25 -0
  73. package/dist/richtext-render.d.ts.map +1 -1
  74. package/dist/richtext-render.js +2 -0
  75. package/dist/richtext.d.ts.map +1 -1
  76. package/dist/richtext.js +2 -0
  77. package/dist/scheduler.interface.d.ts.map +1 -1
  78. package/dist/scheduler.interface.js +2 -0
  79. package/dist/scheduler.stub.d.ts.map +1 -1
  80. package/dist/scheduler.stub.js +2 -0
  81. package/dist/schema-mutator.d.ts +22 -0
  82. package/dist/schema-mutator.d.ts.map +1 -0
  83. package/dist/schema-mutator.js +3 -0
  84. package/dist/search/search.repository.d.ts.map +1 -1
  85. package/dist/search/search.repository.js +2 -0
  86. package/dist/seed-ddl.d.ts +40 -0
  87. package/dist/seed-ddl.d.ts.map +1 -0
  88. package/dist/seed-ddl.js +100 -0
  89. package/dist/seed-layout.d.ts +117 -0
  90. package/dist/seed-layout.d.ts.map +1 -0
  91. package/dist/seed-layout.js +169 -0
  92. package/dist/seed-layout.repository.d.ts +18 -0
  93. package/dist/seed-layout.repository.d.ts.map +1 -0
  94. package/dist/seed-layout.repository.js +3 -0
  95. package/dist/seed-registry.d.ts +23 -1
  96. package/dist/seed-registry.d.ts.map +1 -1
  97. package/dist/seed-registry.js +105 -0
  98. package/dist/seed-validation.d.ts +23 -0
  99. package/dist/seed-validation.d.ts.map +1 -0
  100. package/dist/seed-validation.js +156 -0
  101. package/dist/seed.repository.d.ts +37 -0
  102. package/dist/seed.repository.d.ts.map +1 -0
  103. package/dist/seed.repository.js +3 -0
  104. package/dist/seeds.d.ts.map +1 -1
  105. package/dist/seeds.js +2 -0
  106. package/dist/site-settings.repository.d.ts +16 -0
  107. package/dist/site-settings.repository.d.ts.map +1 -0
  108. package/dist/site-settings.repository.js +3 -0
  109. package/dist/slug-utils.d.ts.map +1 -1
  110. package/dist/slug-utils.js +2 -0
  111. package/dist/storage.d.ts +12 -38
  112. package/dist/storage.d.ts.map +1 -1
  113. package/dist/storage.js +2 -0
  114. package/dist/types.d.ts +80 -1
  115. package/dist/types.d.ts.map +1 -1
  116. package/dist/types.js +2 -0
  117. package/dist/validation.d.ts +23 -17
  118. package/dist/validation.d.ts.map +1 -1
  119. package/dist/validation.js +495 -393
  120. package/dist/webhook-validation.d.ts +3 -0
  121. package/dist/webhook-validation.d.ts.map +1 -0
  122. package/dist/webhook-validation.js +6 -0
  123. package/dist/widget/widget.repository.d.ts.map +1 -1
  124. package/dist/widget/widget.repository.js +2 -0
  125. package/package.json +5 -4
package/dist/engine.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
1
3
  const BRANCH_TYPE_SQL = {
2
4
  text: { sqlType: 'TEXT' },
3
5
  number: { sqlType: 'REAL' },
@@ -7,6 +9,7 @@ const BRANCH_TYPE_SQL = {
7
9
  richtext: { sqlType: 'TEXT' },
8
10
  file: { sqlType: 'TEXT' }, // URL singolo o JSON array di URL
9
11
  tags: { sqlType: 'TEXT' }, // JSON array di stringhe
12
+ relation: { sqlType: 'TEXT' }, // FK reference stored as TEXT (id of the target row)
10
13
  };
11
14
  const SYSTEM_COLUMNS = new Set(['id', 'slug', 'status', 'created_at', 'updated_at']);
12
15
  // ---- Private helpers ----
@@ -21,7 +24,7 @@ function isValidColumn(seed, col) {
21
24
  return true;
22
25
  return seed.branches.some(b => b.alias === col);
23
26
  }
24
- function indexableSearchBranches(seed) {
27
+ export function indexableSearchBranches(seed) {
25
28
  return seed.branches.filter(b => (b.type === 'text' || b.type === 'richtext') && b.policies?.search !== false);
26
29
  }
27
30
  function isAssetListBranch(branch) {
@@ -69,6 +72,29 @@ function normalizeAssetListValue(rawValue) {
69
72
  }
70
73
  return [...new Set(normalized)];
71
74
  }
75
+ // ---- FK helpers ----
76
+ const DEFAULT_ON_DELETE_RULE = 'SET NULL';
77
+ /**
78
+ * Returns the trailing FK fragment for a single-value relation branch, or ''
79
+ * if the branch is not a relation. Throws when a single-value relation branch
80
+ * is missing `targetSeed` — that is a programmer error that must surface at
81
+ * DDL time rather than producing silently invalid SQL.
82
+ *
83
+ * Multi-relation branches (multiple: true) produce NO column on the parent
84
+ * table; they live in a dedicated junction table. Returns '' for them.
85
+ */
86
+ function buildForeignKeyClause(branch) {
87
+ if (branch.type !== 'relation')
88
+ return '';
89
+ // Many-to-many branches: no column on parent table — see generateJunctionTable()
90
+ if (branch.multiple === true)
91
+ return '';
92
+ if (!branch.targetSeed) {
93
+ throw new Error(`Branch "${branch.alias}" is of type 'relation' but has no targetSeed`);
94
+ }
95
+ const onDeleteRule = branch.onDelete ?? DEFAULT_ON_DELETE_RULE;
96
+ return ` REFERENCES content_${branch.targetSeed}(id) ON DELETE ${onDeleteRule}`;
97
+ }
72
98
  // ---- DDL Generators ----
73
99
  /**
74
100
  * Generates `CREATE TABLE IF NOT EXISTS content_{slug}` with system columns
@@ -83,12 +109,17 @@ export function generateCreateTable(seed) {
83
109
  ` status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),`,
84
110
  ];
85
111
  for (const branch of seed.branches) {
112
+ // Many-to-many relation branches produce no column on the parent table;
113
+ // they are represented by a dedicated junction table. See generateJunctionTable().
114
+ if (branch.type === 'relation' && branch.multiple === true)
115
+ continue;
86
116
  const { sqlType } = BRANCH_TYPE_SQL[branch.type];
87
117
  let col = ` ${branch.alias} ${sqlType}`;
88
118
  if (branch.requiredOnCreate)
89
119
  col += ' NOT NULL';
90
120
  if (branch.type === 'boolean')
91
121
  col += ` CHECK (${branch.alias} IN (0, 1))`;
122
+ col += buildForeignKeyClause(branch);
92
123
  lines.push(col + ',');
93
124
  }
94
125
  lines.push(` created_at INTEGER NOT NULL DEFAULT (unixepoch()),`);
@@ -112,6 +143,9 @@ export function generateDraftTable(seed) {
112
143
  ` REFERENCES ${mainTable}(id) ON DELETE CASCADE,`,
113
144
  ];
114
145
  for (const branch of seed.branches) {
146
+ // Many-to-many relation branches mirror in their own junction drafts table.
147
+ if (branch.type === 'relation' && branch.multiple === true)
148
+ continue;
115
149
  const { sqlType } = BRANCH_TYPE_SQL[branch.type];
116
150
  let col = ` ${branch.alias} ${sqlType}`;
117
151
  // boolean CHECK: in SQLite, NULL IN (0,1) → NULL, which passes the CHECK (only FALSE fails it)
@@ -129,7 +163,7 @@ export function generateDraftTable(seed) {
129
163
  */
130
164
  export function generateAddColumn(seed, branch) {
131
165
  const { sqlType } = BRANCH_TYPE_SQL[branch.type];
132
- return `ALTER TABLE ${tableName(seed)} ADD COLUMN ${branch.alias} ${sqlType};`;
166
+ return `ALTER TABLE ${tableName(seed)} ADD COLUMN ${branch.alias} ${sqlType}${buildForeignKeyClause(branch)};`;
133
167
  }
134
168
  /**
135
169
  * Generates B-tree indexes for status, created_at and every filterable Branch
@@ -143,9 +177,15 @@ export function generateIndexes(seed) {
143
177
  `CREATE INDEX IF NOT EXISTS idx_${slug}_created_at ON ${table}(created_at);`,
144
178
  ];
145
179
  for (const branch of seed.branches) {
146
- if (branch.policies?.filter === false)
180
+ // Multi-relation branches have no column on the parent table; their indexes
181
+ // live on the junction table. See generateJunctionIndexes().
182
+ if (branch.type === 'relation' && branch.multiple === true)
147
183
  continue;
148
- if (['text', 'number', 'date', 'boolean'].includes(branch.type)) {
184
+ const isRelation = branch.type === 'relation';
185
+ // Relation indexes are a system concern (JOIN performance) — bypass editorial filter policy
186
+ if (!isRelation && branch.policies?.filter === false)
187
+ continue;
188
+ if (['text', 'number', 'date', 'boolean', 'relation'].includes(branch.type)) {
149
189
  indexes.push(`CREATE INDEX IF NOT EXISTS idx_${slug}_${branch.alias} ON ${table}(${branch.alias});`);
150
190
  }
151
191
  }
@@ -212,6 +252,7 @@ export function generateFtsTriggers(seed) {
212
252
  * Never uses json_extract — every column is a real column.
213
253
  * Unknown columns in filters/orderBy are ignored (fail-closed).
214
254
  */
255
+ //TODO: buildSelectQuery ha troppe responsabilità, va suddiviso in più funzioni
215
256
  export function buildSelectQuery(seed, options = {}) {
216
257
  const table = tableName(seed);
217
258
  const { filters = [], orderBy, pagination, status, search, fields } = options;
@@ -278,6 +319,7 @@ export function buildSelectQuery(seed, options = {}) {
278
319
  }
279
320
  return { sql, bindings };
280
321
  }
322
+ //TODO: normalizeFilterValue ha troppe responsabilità, va suddiviso in più funzioni
281
323
  function normalizeFilterValue(type, value) {
282
324
  if (value === null || value === undefined)
283
325
  return null;
@@ -286,7 +328,7 @@ function normalizeFilterValue(type, value) {
286
328
  return value;
287
329
  if (typeof value === 'string' && value.trim() !== '') {
288
330
  const d = new Date(value);
289
- return isNaN(d.getTime()) ? null : Math.floor(d.getTime() / 1000);
331
+ return Number.isNaN(d.getTime()) ? null : Math.floor(d.getTime() / 1000);
290
332
  }
291
333
  return null;
292
334
  }
@@ -298,12 +340,13 @@ function normalizeFilterValue(type, value) {
298
340
  return value;
299
341
  if (typeof value === 'string' && value.trim() !== '') {
300
342
  const n = Number(value);
301
- return isNaN(n) ? null : n;
343
+ return Number.isNaN(n) ? null : n;
302
344
  }
303
345
  return null;
304
346
  }
305
347
  return value;
306
348
  }
349
+ //TODO: buildFilterCondition ha troppe responsabilità, va suddiviso in più funzioni
307
350
  function buildFilterCondition(col, type, cond, bindings) {
308
351
  const { op, value } = cond;
309
352
  if (op === 'is_empty') {
@@ -377,7 +420,10 @@ export function getExpectedColumns(seed) {
377
420
  { name: 'id', sqlType: 'TEXT', notNull: true, isPk: true },
378
421
  { name: 'slug', sqlType: 'TEXT', notNull: true, isPk: false },
379
422
  { name: 'status', sqlType: 'TEXT', notNull: true, isPk: false },
380
- ...seed.branches.map(b => ({
423
+ // Multi-relation branches produce no column on the parent table.
424
+ ...seed.branches
425
+ .filter(b => !(b.type === 'relation' && b.multiple === true))
426
+ .map(b => ({
381
427
  name: b.alias,
382
428
  sqlType: BRANCH_TYPE_SQL[b.type].sqlType,
383
429
  notNull: b.requiredOnCreate ?? false,
@@ -392,6 +438,7 @@ export function getExpectedColumns(seed) {
392
438
  * Serializes a value for writing to the DB.
393
439
  * boolean → 0/1 | date → Unix timestamp | json/asset-list → JSON string
394
440
  */
441
+ //TODO: serializeForDb ha troppe responsabilità, va suddiviso in più funzioni
395
442
  export function serializeForDb(branch, value) {
396
443
  if (value === null || value === undefined)
397
444
  return null;
@@ -407,7 +454,7 @@ export function serializeForDb(branch, value) {
407
454
  return value;
408
455
  if (typeof value === 'string') {
409
456
  const d = new Date(value);
410
- if (isNaN(d.getTime()))
457
+ if (Number.isNaN(d.getTime()))
411
458
  return null;
412
459
  if (branch.format === 'date') {
413
460
  const midnight = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
@@ -426,6 +473,204 @@ export function serializeForDb(branch, value) {
426
473
  return typeof value === 'string' ? value : typeof value === 'number' ? value : null;
427
474
  }
428
475
  }
476
+ // ---- Junction table DDL (many-to-many relations, Sprint 5) ----
477
+ /**
478
+ * Returns the junction table name for a many-to-many relation branch.
479
+ * Format: `rel_<seedSlug>_<branchAlias>`.
480
+ */
481
+ export function junctionTableName(seedSlug, branchAlias) {
482
+ return `rel_${seedSlug}_${branchAlias}`;
483
+ }
484
+ /**
485
+ * Generates `CREATE TABLE IF NOT EXISTS rel_<seed>_<alias>` for a multi-relation
486
+ * branch. The parent FK always cascades (parent lifecycle owns the edge);
487
+ * the target FK uses `branch.onDelete` (default CASCADE).
488
+ *
489
+ * @throws {Error} when called on a non-multi-relation branch
490
+ * @throws {Error} when `branch.targetSeed` is missing
491
+ */
492
+ export function generateJunctionTable(seed, branch) {
493
+ if (branch.type !== 'relation' || branch.multiple !== true) {
494
+ throw new Error(`Branch "${branch.alias}" is not a multi-relation`);
495
+ }
496
+ if (!branch.targetSeed) {
497
+ throw new Error(`Branch "${branch.alias}" has no targetSeed`);
498
+ }
499
+ const onDeleteRule = branch.onDelete ?? 'CASCADE';
500
+ const table = junctionTableName(seed.slug, branch.alias);
501
+ return [
502
+ `CREATE TABLE IF NOT EXISTS ${table} (`,
503
+ ` parent_id TEXT NOT NULL REFERENCES content_${seed.slug}(id) ON DELETE CASCADE,`,
504
+ ` target_id TEXT NOT NULL REFERENCES content_${branch.targetSeed}(id) ON DELETE ${onDeleteRule},`,
505
+ ` position INTEGER NOT NULL DEFAULT 0,`,
506
+ ` created_at INTEGER NOT NULL DEFAULT (unixepoch()),`,
507
+ ` PRIMARY KEY (parent_id, target_id)`,
508
+ `);`,
509
+ ].join('\n');
510
+ }
511
+ /**
512
+ * Generates the two B-tree indexes for a junction table:
513
+ * one on `parent_id` (list entries by parent) and one on `target_id`
514
+ * (cascade checks from target side).
515
+ */
516
+ export function generateJunctionIndexes(seed, branch) {
517
+ const table = junctionTableName(seed.slug, branch.alias);
518
+ return [
519
+ `CREATE INDEX IF NOT EXISTS idx_${table}_parent ON ${table}(parent_id);`,
520
+ `CREATE INDEX IF NOT EXISTS idx_${table}_target ON ${table}(target_id);`,
521
+ ];
522
+ }
523
+ /**
524
+ * Generates the drafts junction table `rel_<seed>_<alias>_drafts` for a
525
+ * many-to-many relation branch. Note: no FK on `target_id` — targets may be
526
+ * deleted between draft save and publish; the FK would be wrong here.
527
+ * Returns null when `seed.allowDrafts` is falsy.
528
+ */
529
+ export function generateJunctionDraftTable(seed, branch) {
530
+ if (!seed.allowDrafts)
531
+ return null;
532
+ if (branch.type !== 'relation' || branch.multiple !== true)
533
+ return null;
534
+ const table = `${junctionTableName(seed.slug, branch.alias)}_drafts`;
535
+ const mainDraftTable = `content_${seed.slug}_drafts`;
536
+ return [
537
+ `CREATE TABLE IF NOT EXISTS ${table} (`,
538
+ ` entry_id TEXT NOT NULL REFERENCES ${mainDraftTable}(entry_id) ON DELETE CASCADE,`,
539
+ ` target_id TEXT NOT NULL,`,
540
+ ` position INTEGER NOT NULL DEFAULT 0,`,
541
+ ` PRIMARY KEY (entry_id, target_id)`,
542
+ `);`,
543
+ ].join('\n');
544
+ }
545
+ // ---- Destructive DDL Generators (Sprint 06 — Danger Zone) ----
546
+ //
547
+ // Every generator below emits IRREVERSIBLE, data-destroying SQL. They are pure
548
+ // string builders (no DB access); the actual execution goes through
549
+ // ISchemaMutator's dedicated destructive methods (dropTable/dropColumn/
550
+ // renameColumn/execDestructive), which re-validate every identifier before
551
+ // interpolation. These statements MUST NEVER be passed to execDdl (additive-only).
552
+ function draftTableName(seed) {
553
+ return `content_${seed.slug}_drafts`;
554
+ }
555
+ /**
556
+ * Returns every `DROP TABLE IF EXISTS` needed to fully remove a content type:
557
+ * the main `content_{slug}` table, its `fts_{slug}` virtual table, the
558
+ * `content_{slug}_drafts` mirror (when drafts are enabled), and each
559
+ * multi-relation junction table `rel_{slug}_{alias}` (+ its `_drafts` mirror).
560
+ *
561
+ * FTS triggers reference the main table; dropping the main table also drops
562
+ * its triggers in SQLite, but we drop the FTS table explicitly so no orphaned
563
+ * virtual table survives. Order: junction (children) → drafts → fts → main.
564
+ */
565
+ export function generateDropTable(seed) {
566
+ const stmts = [];
567
+ // Junction tables first (they FK-reference the main/draft tables).
568
+ for (const branch of seed.branches) {
569
+ if (branch.type !== 'relation' || branch.multiple !== true)
570
+ continue;
571
+ const jt = junctionTableName(seed.slug, branch.alias);
572
+ stmts.push(`DROP TABLE IF EXISTS ${jt}_drafts;`);
573
+ stmts.push(`DROP TABLE IF EXISTS ${jt};`);
574
+ }
575
+ if (seed.allowDrafts) {
576
+ stmts.push(`DROP TABLE IF EXISTS ${draftTableName(seed)};`);
577
+ }
578
+ // FTS triggers + virtual table.
579
+ if (indexableSearchBranches(seed).length > 0) {
580
+ const slug = seed.slug;
581
+ stmts.push(`DROP TRIGGER IF EXISTS fts_${slug}_insert;`);
582
+ stmts.push(`DROP TRIGGER IF EXISTS fts_${slug}_update;`);
583
+ stmts.push(`DROP TRIGGER IF EXISTS fts_${slug}_delete;`);
584
+ stmts.push(`DROP TABLE IF EXISTS ${ftsTableName(seed)};`);
585
+ }
586
+ stmts.push(`DROP TABLE IF EXISTS ${tableName(seed)};`);
587
+ return stmts;
588
+ }
589
+ /**
590
+ * Returns the `DROP COLUMN` statements for removing a single field.
591
+ *
592
+ * - For a multi-relation branch (no column on the parent table) it drops the
593
+ * junction table `rel_{slug}_{alias}` (+ `_drafts`) instead.
594
+ * - For a normal branch it drops the column from `content_{slug}` and, when
595
+ * drafts are enabled, the mirror column from `content_{slug}_drafts`.
596
+ * - For an orphan column (no matching branch in the definition) it falls back
597
+ * to a plain `ALTER TABLE … DROP COLUMN`, never touching drafts.
598
+ *
599
+ * Callers must rebuild FTS separately (planFtsRebuild) when the dropped column
600
+ * was searchable, since SQLite cannot ALTER an fts5 table's columns.
601
+ */
602
+ export function generateDropColumn(seed, alias) {
603
+ const branch = seed.branches.find(b => b.alias === alias);
604
+ if (branch && branch.type === 'relation' && branch.multiple === true) {
605
+ const jt = junctionTableName(seed.slug, alias);
606
+ return [
607
+ `DROP TABLE IF EXISTS ${jt}_drafts;`,
608
+ `DROP TABLE IF EXISTS ${jt};`,
609
+ ];
610
+ }
611
+ const stmts = [`ALTER TABLE ${tableName(seed)} DROP COLUMN ${alias};`];
612
+ // Drop the mirror column from the drafts table when the branch lives there.
613
+ if (branch && seed.allowDrafts) {
614
+ stmts.push(`ALTER TABLE ${draftTableName(seed)} DROP COLUMN ${alias};`);
615
+ }
616
+ return stmts;
617
+ }
618
+ /**
619
+ * Returns the `RENAME COLUMN` statements for renaming a field's alias while
620
+ * keeping the stable `branch.id`. Renames the column on `content_{slug}` and,
621
+ * when drafts are enabled, the mirror column on `content_{slug}_drafts`.
622
+ *
623
+ * For a multi-relation branch the alias is part of the junction table name, so
624
+ * the junction table (+ `_drafts`) is renamed instead of a column.
625
+ *
626
+ * Callers must rebuild FTS separately (planFtsRebuild) when the renamed column
627
+ * was searchable.
628
+ */
629
+ export function generateRenameColumn(seed, from, to) {
630
+ const branch = seed.branches.find(b => b.alias === from);
631
+ if (branch && branch.type === 'relation' && branch.multiple === true) {
632
+ const oldJt = junctionTableName(seed.slug, from);
633
+ const newJt = junctionTableName(seed.slug, to);
634
+ const stmts = [`ALTER TABLE ${oldJt} RENAME TO ${newJt};`];
635
+ if (seed.allowDrafts) {
636
+ stmts.push(`ALTER TABLE ${oldJt}_drafts RENAME TO ${newJt}_drafts;`);
637
+ }
638
+ return stmts;
639
+ }
640
+ const stmts = [`ALTER TABLE ${tableName(seed)} RENAME COLUMN ${from} TO ${to};`];
641
+ if (branch && seed.allowDrafts) {
642
+ stmts.push(`ALTER TABLE ${draftTableName(seed)} RENAME COLUMN ${from} TO ${to};`);
643
+ }
644
+ return stmts;
645
+ }
646
+ /**
647
+ * Returns the add/copy/drop/rename statements that change a column's SQL type
648
+ * in place, preserving data via `CAST`. This is the documented simpler
649
+ * alternative to SQLite's full 12-step table rebuild.
650
+ *
651
+ * The `branch` passed in is the *target* definition (already carrying the new
652
+ * `type`); its SQL type is read from BRANCH_TYPE_SQL. Multi-relation branches
653
+ * have no column to retype and are rejected. Mirrors the change onto the drafts
654
+ * table when drafts are enabled. Callers rebuild FTS separately when needed.
655
+ */
656
+ export function generateRetypeColumn(seed, branch) {
657
+ if (branch.type === 'relation' && branch.multiple === true) {
658
+ throw new Error(`Branch "${branch.alias}" is a multi-relation and has no column to retype`);
659
+ }
660
+ const { sqlType } = BRANCH_TYPE_SQL[branch.type];
661
+ const alias = branch.alias;
662
+ const tmp = `__retype_${alias}`;
663
+ const rebuild = (table) => [
664
+ `ALTER TABLE ${table} ADD COLUMN ${tmp} ${sqlType};`,
665
+ `UPDATE ${table} SET ${tmp} = CAST(${alias} AS ${sqlType});`,
666
+ `ALTER TABLE ${table} DROP COLUMN ${alias};`,
667
+ `ALTER TABLE ${table} RENAME COLUMN ${tmp} TO ${alias};`,
668
+ ];
669
+ const stmts = rebuild(tableName(seed));
670
+ if (seed.allowDrafts)
671
+ stmts.push(...rebuild(draftTableName(seed)));
672
+ return stmts;
673
+ }
429
674
  /**
430
675
  * Deserializes a value read from the DB for the API response.
431
676
  * 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object
@@ -0,0 +1,13 @@
1
+ export type FileAccept = 'image' | 'document' | 'any';
2
+ export declare const IMAGE_EXTENSIONS: Set<string>;
3
+ export declare const BLOCKED_IMAGE_EXTENSIONS: Set<string>;
4
+ export declare const DOCUMENT_EXTENSIONS: Set<string>;
5
+ export declare const ARCHIVE_EXTENSIONS: Set<string>;
6
+ export declare const IMAGE_MIME_TYPES: readonly ["image/jpeg", "image/png", "image/gif", "image/webp", "image/avif", "image/bmp", "image/x-icon", "image/vnd.microsoft.icon"];
7
+ export declare const BLOCKED_IMAGE_MIME_TYPES: readonly ["image/svg+xml", "image/svg"];
8
+ export declare const DOCUMENT_MIME_TYPES: readonly ["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "text/plain", "text/csv", "text/markdown"];
9
+ export declare const ARCHIVE_MIME_TYPES: readonly ["application/zip", "application/x-zip-compressed", "application/x-7z-compressed", "application/x-tar", "application/gzip", "application/json"];
10
+ export declare function extensionFromUrl(url: string): string | null;
11
+ export declare function isExtensionAccepted(ext: string | null, accept: FileAccept): boolean;
12
+ export declare function isMimeAccepted(mime: string, accept: FileAccept): boolean;
13
+ //# sourceMappingURL=file-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-types.d.ts","sourceRoot":"","sources":["../src/file-types.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,KAAK,CAAA;AAKrD,eAAO,MAAM,gBAAgB,aAAuE,CAAA;AACpG,eAAO,MAAM,wBAAwB,aAA2B,CAAA;AAChE,eAAO,MAAM,mBAAmB,aAAoF,CAAA;AACpH,eAAO,MAAM,kBAAkB,aAA8C,CAAA;AAI7E,eAAO,MAAM,gBAAgB,wIASnB,CAAA;AAGV,eAAO,MAAM,wBAAwB,yCAA0C,CAAA;AAC/E,eAAO,MAAM,mBAAmB,yXAWtB,CAAA;AACV,eAAO,MAAM,kBAAkB,0JAOrB,CAAA;AAEV,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS3D;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAMnF;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAcxE"}
@@ -0,0 +1,85 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ // SVG is intentionally excluded: SVG can embed <script> tags and is rendered
4
+ // inline by browsers when served as image/svg+xml on a same-origin route,
5
+ // creating a stored XSS vector (see security audit finding #4).
6
+ export const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'bmp', 'ico']);
7
+ export const BLOCKED_IMAGE_EXTENSIONS = new Set(['svg', 'svgz']);
8
+ export const DOCUMENT_EXTENSIONS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'md']);
9
+ export const ARCHIVE_EXTENSIONS = new Set(['zip', '7z', 'tar', 'gz', 'json']);
10
+ // Explicitly enumerate safe image MIME types instead of using the wildcard
11
+ // `image/*` prefix, which would permit `image/svg+xml` and other XSS vectors.
12
+ export const IMAGE_MIME_TYPES = [
13
+ 'image/jpeg',
14
+ 'image/png',
15
+ 'image/gif',
16
+ 'image/webp',
17
+ 'image/avif',
18
+ 'image/bmp',
19
+ 'image/x-icon',
20
+ 'image/vnd.microsoft.icon',
21
+ ];
22
+ // Blocked image MIME types — listed explicitly so upload code can reject them
23
+ // with a clear error rather than a generic "file type not allowed".
24
+ export const BLOCKED_IMAGE_MIME_TYPES = ['image/svg+xml', 'image/svg'];
25
+ export const DOCUMENT_MIME_TYPES = [
26
+ 'application/pdf',
27
+ 'application/msword',
28
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
29
+ 'application/vnd.ms-excel',
30
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
31
+ 'application/vnd.ms-powerpoint',
32
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
33
+ 'text/plain',
34
+ 'text/csv',
35
+ 'text/markdown',
36
+ ];
37
+ export const ARCHIVE_MIME_TYPES = [
38
+ 'application/zip',
39
+ 'application/x-zip-compressed',
40
+ 'application/x-7z-compressed',
41
+ 'application/x-tar',
42
+ 'application/gzip',
43
+ 'application/json',
44
+ ];
45
+ export function extensionFromUrl(url) {
46
+ try {
47
+ const path = new URL(url).pathname;
48
+ const dot = path.lastIndexOf('.');
49
+ if (dot < 0 || dot === path.length - 1)
50
+ return null;
51
+ return path.slice(dot + 1).toLowerCase();
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export function isExtensionAccepted(ext, accept) {
58
+ if (accept === 'any')
59
+ return true;
60
+ if (ext == null)
61
+ return false;
62
+ if (accept === 'image')
63
+ return IMAGE_EXTENSIONS.has(ext);
64
+ if (accept === 'document')
65
+ return DOCUMENT_EXTENSIONS.has(ext);
66
+ return false;
67
+ }
68
+ export function isMimeAccepted(mime, accept) {
69
+ // Normalise: strip parameters (e.g. "image/jpeg; charset=utf-8" → "image/jpeg")
70
+ const normalised = mime.split(';')[0].trim().toLowerCase();
71
+ // Unconditionally reject SVG regardless of accept mode — SVG can carry
72
+ // embedded scripts and is rendered by browsers when served inline.
73
+ if (BLOCKED_IMAGE_MIME_TYPES.includes(normalised))
74
+ return false;
75
+ if (accept === 'any') {
76
+ return IMAGE_MIME_TYPES.includes(normalised)
77
+ || DOCUMENT_MIME_TYPES.includes(normalised)
78
+ || ARCHIVE_MIME_TYPES.includes(normalised);
79
+ }
80
+ if (accept === 'image')
81
+ return IMAGE_MIME_TYPES.includes(normalised);
82
+ if (accept === 'document')
83
+ return DOCUMENT_MIME_TYPES.includes(normalised);
84
+ return false;
85
+ }
@@ -10,6 +10,13 @@ export interface IIdGenerator {
10
10
  * Test implementations return deterministic values for snapshot assertions.
11
11
  */
12
12
  uuid(): string;
13
+ /**
14
+ * Returns true when `value` has the exact shape produced by `uuid()`.
15
+ * The ONLY place in the codebase that knows the id format. Never inline
16
+ * a regex; always go through this method when validating a relation id,
17
+ * a route param, or any user-supplied id.
18
+ */
19
+ isValid(value: unknown): value is string;
13
20
  }
14
21
  /**
15
22
  * Production singleton. Delegates to the host runtime CSPRNG-backed UUID
@@ -1 +1 @@
1
- {"version":3,"file":"id-generator.d.ts","sourceRoot":"","sources":["../src/id-generator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,IAAI,IAAI,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,YAE/B,CAAA"}
1
+ {"version":3,"file":"id-generator.d.ts","sourceRoot":"","sources":["../src/id-generator.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,IAAI,IAAI,MAAM,CAAA;IAEd;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAA;CACzC;AAMD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,YAI/B,CAAA"}
@@ -1,7 +1,12 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ // Internal — never exported. Callers depend on the interface, not the shape.
4
+ const SYSTEM_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1
5
  /**
2
6
  * Production singleton. Delegates to the host runtime CSPRNG-backed UUID
3
7
  * generator. Stateless and therefore safe to share across requests.
4
8
  */
5
9
  export const SystemIdGenerator = {
6
10
  uuid: () => crypto.randomUUID(),
11
+ isValid: (value) => typeof value === 'string' && SYSTEM_ID_REGEX.test(value),
7
12
  };
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.repository.d.ts","sourceRoot":"","sources":["../src/idempotency.repository.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;IACtD,KAAK,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC"}
1
+ {"version":3,"file":"idempotency.repository.d.ts","sourceRoot":"","sources":["../src/idempotency.repository.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;IACtD,KAAK,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC"}
@@ -1 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
1
3
  export {};
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * @module @beechcms/core
9
9
  */
10
10
  export * from './types.js';
11
+ export * from './file-types.js';
11
12
  export * from './define-seed.js';
12
13
  export * from './seeds.js';
13
14
  export * from './engine.js';
@@ -41,7 +42,17 @@ export * from './automations.types.js';
41
42
  export * from './automations-grammar-words.js';
42
43
  export * from './automations.runner.interface.js';
43
44
  export * from './automations.repository.interface.js';
44
- export * from './automations.runner.stub.js';
45
45
  export * from './scheduler.interface.js';
46
46
  export * from './scheduler.stub.js';
47
+ export * from './webhook-validation.js';
48
+ export * from './relations.js';
49
+ export * from './site-settings.repository.js';
50
+ export * from './demo-data.repository.js';
51
+ export * from './seed-layout.js';
52
+ export * from './seed-layout.repository.js';
53
+ export * from './layout-permissions.js';
54
+ export * from './seed.repository.js';
55
+ export * from './seed-validation.js';
56
+ export * from './seed-ddl.js';
57
+ export * from './schema-mutator.js';
47
58
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,yBAAyB,CAAA;AACvC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2CAA2C,CAAA;AACzD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,oCAAoC,CAAA;AAClD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA;AACtC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mCAAmC,CAAA;AACjD,cAAc,uCAAuC,CAAA;AACrD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,yBAAyB,CAAA;AACvC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2CAA2C,CAAA;AACzD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,oCAAoC,CAAA;AAClD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA;AACtC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mCAAmC,CAAA;AACjD,cAAc,uCAAuC,CAAA;AACrD,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,kBAAkB,CAAA;AAChC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
1
3
  /**
2
4
  * @beechcms/core - Botanical Engine
3
5
  *
@@ -8,6 +10,7 @@
8
10
  * @module @beechcms/core
9
11
  */
10
12
  export * from './types.js';
13
+ export * from './file-types.js';
11
14
  export * from './define-seed.js';
12
15
  export * from './seeds.js';
13
16
  export * from './engine.js';
@@ -41,6 +44,16 @@ export * from './automations.types.js';
41
44
  export * from './automations-grammar-words.js';
42
45
  export * from './automations.runner.interface.js';
43
46
  export * from './automations.repository.interface.js';
44
- export * from './automations.runner.stub.js';
45
47
  export * from './scheduler.interface.js';
46
48
  export * from './scheduler.stub.js';
49
+ export * from './webhook-validation.js';
50
+ export * from './relations.js';
51
+ export * from './site-settings.repository.js';
52
+ export * from './demo-data.repository.js';
53
+ export * from './seed-layout.js';
54
+ export * from './seed-layout.repository.js';
55
+ export * from './layout-permissions.js';
56
+ export * from './seed.repository.js';
57
+ export * from './seed-validation.js';
58
+ export * from './seed-ddl.js';
59
+ export * from './schema-mutator.js';
@@ -0,0 +1,3 @@
1
+ export declare const ROLES_ALLOWED_TO_EDIT_LAYOUT: ReadonlyArray<string>;
2
+ export declare function canEditLayout(role: string | undefined | null): boolean;
3
+ //# sourceMappingURL=layout-permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout-permissions.d.ts","sourceRoot":"","sources":["../src/layout-permissions.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,4BAA4B,EAAE,aAAa,CAAC,MAAM,CAAa,CAAA;AAE5E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAGtE"}
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ // NOTE: change this list to extend write-access to other roles
4
+ // (e.g. add 'editor', or introduce a fine-grained 'layout:edit' permission).
5
+ // Single source of truth — used by both API guards and dashboard buttons.
6
+ export const ROLES_ALLOWED_TO_EDIT_LAYOUT = ['admin'];
7
+ export function canEditLayout(role) {
8
+ if (!role)
9
+ return false;
10
+ return ROLES_ALLOWED_TO_EDIT_LAYOUT.includes(role);
11
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"media.repository.d.ts","sourceRoot":"","sources":["../src/media.repository.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAEnD;;OAEG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpC;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAEnG;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC;;OAEG;IACH,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACpC"}
1
+ {"version":3,"file":"media.repository.d.ts","sourceRoot":"","sources":["../src/media.repository.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAEnD;;OAEG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpC;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAEnG;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC;;OAEG;IACH,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACpC"}
@@ -1 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
1
3
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"notification-service.d.ts","sourceRoot":"","sources":["../../src/notifications/notification-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAEpE,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,MAAM,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC7D"}
1
+ {"version":3,"file":"notification-service.d.ts","sourceRoot":"","sources":["../../src/notifications/notification-service.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAEpE,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,MAAM,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC7D"}
@@ -1 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
1
3
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"notification.repository.d.ts","sourceRoot":"","sources":["../../src/notifications/notification.repository.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAEvE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,gBAAgB,CAAA;IACtB,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAElD;;;;;;OAMG;IACH,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAEnC;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,IAAI,GAAG,WAAW,GAAG,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAExF,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9C"}
1
+ {"version":3,"file":"notification.repository.d.ts","sourceRoot":"","sources":["../../src/notifications/notification.repository.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAEvE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,gBAAgB,CAAA;IACtB,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAElD;;;;;;OAMG;IACH,KAAK,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAEnC;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,IAAI,GAAG,WAAW,GAAG,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAExF,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9C"}