@actuate-media/cms-core 0.25.1 → 0.27.0

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 (69) hide show
  1. package/dist/__tests__/api/collections-discovery.test.js +47 -0
  2. package/dist/__tests__/api/collections-discovery.test.js.map +1 -1
  3. package/dist/__tests__/api/page-sections-routes.test.d.ts +2 -0
  4. package/dist/__tests__/api/page-sections-routes.test.d.ts.map +1 -0
  5. package/dist/__tests__/api/page-sections-routes.test.js +271 -0
  6. package/dist/__tests__/api/page-sections-routes.test.js.map +1 -0
  7. package/dist/__tests__/api/stats-collection-filter.test.d.ts +2 -0
  8. package/dist/__tests__/api/stats-collection-filter.test.d.ts.map +1 -0
  9. package/dist/__tests__/api/stats-collection-filter.test.js +190 -0
  10. package/dist/__tests__/api/stats-collection-filter.test.js.map +1 -0
  11. package/dist/__tests__/api/stats-routes.test.d.ts +2 -0
  12. package/dist/__tests__/api/stats-routes.test.d.ts.map +1 -0
  13. package/dist/__tests__/api/stats-routes.test.js +251 -0
  14. package/dist/__tests__/api/stats-routes.test.js.map +1 -0
  15. package/dist/__tests__/content-health/scanner.test.d.ts +2 -0
  16. package/dist/__tests__/content-health/scanner.test.d.ts.map +1 -0
  17. package/dist/__tests__/content-health/scanner.test.js +151 -0
  18. package/dist/__tests__/content-health/scanner.test.js.map +1 -0
  19. package/dist/__tests__/diagnostics/api-metrics.test.d.ts +2 -0
  20. package/dist/__tests__/diagnostics/api-metrics.test.d.ts.map +1 -0
  21. package/dist/__tests__/diagnostics/api-metrics.test.js +153 -0
  22. package/dist/__tests__/diagnostics/api-metrics.test.js.map +1 -0
  23. package/dist/api/handler-factory.d.ts.map +1 -1
  24. package/dist/api/handler-factory.js +31 -1
  25. package/dist/api/handler-factory.js.map +1 -1
  26. package/dist/api/handlers.d.ts.map +1 -1
  27. package/dist/api/handlers.js +678 -5
  28. package/dist/api/handlers.js.map +1 -1
  29. package/dist/config/types.d.ts +8 -0
  30. package/dist/config/types.d.ts.map +1 -1
  31. package/dist/content-health/cron.d.ts +67 -0
  32. package/dist/content-health/cron.d.ts.map +1 -0
  33. package/dist/content-health/cron.js +167 -0
  34. package/dist/content-health/cron.js.map +1 -0
  35. package/dist/content-health/scanner.d.ts +118 -0
  36. package/dist/content-health/scanner.d.ts.map +1 -0
  37. package/dist/content-health/scanner.js +263 -0
  38. package/dist/content-health/scanner.js.map +1 -0
  39. package/dist/diagnostics/api-metrics.d.ts +135 -0
  40. package/dist/diagnostics/api-metrics.d.ts.map +1 -0
  41. package/dist/diagnostics/api-metrics.js +374 -0
  42. package/dist/diagnostics/api-metrics.js.map +1 -0
  43. package/dist/sections/__tests__/sections.test.d.ts +2 -0
  44. package/dist/sections/__tests__/sections.test.d.ts.map +1 -0
  45. package/dist/sections/__tests__/sections.test.js +215 -0
  46. package/dist/sections/__tests__/sections.test.js.map +1 -0
  47. package/dist/sections/helpers.d.ts +62 -0
  48. package/dist/sections/helpers.d.ts.map +1 -0
  49. package/dist/sections/helpers.js +202 -0
  50. package/dist/sections/helpers.js.map +1 -0
  51. package/dist/sections/index.d.ts +13 -0
  52. package/dist/sections/index.d.ts.map +1 -0
  53. package/dist/sections/index.js +12 -0
  54. package/dist/sections/index.js.map +1 -0
  55. package/dist/sections/registry.d.ts +27 -0
  56. package/dist/sections/registry.d.ts.map +1 -0
  57. package/dist/sections/registry.js +349 -0
  58. package/dist/sections/registry.js.map +1 -0
  59. package/dist/sections/types.d.ts +127 -0
  60. package/dist/sections/types.d.ts.map +1 -0
  61. package/dist/sections/types.js +21 -0
  62. package/dist/sections/types.js.map +1 -0
  63. package/dist/sections/validate.d.ts +10 -0
  64. package/dist/sections/validate.d.ts.map +1 -0
  65. package/dist/sections/validate.js +92 -0
  66. package/dist/sections/validate.js.map +1 -0
  67. package/package.json +6 -1
  68. package/prisma/migrations/0007_dashboard_metrics/migration.sql +74 -0
  69. package/prisma/schema.prisma +180 -73
@@ -0,0 +1,374 @@
1
+ /**
2
+ * API request instrumentation that powers the Dashboard "Delivery API"
3
+ * tiles (request volume / error rate / avg latency / p95).
4
+ *
5
+ * ## Why per-minute aggregated buckets and not per-request rows
6
+ *
7
+ * A single 24-hour window at 100 req/s on 20 routes is ~17M raw rows,
8
+ * but the same window aggregated to (minute × route × status-bucket)
9
+ * is ≤ 1440 × 20 × 5 = 144k rows — and in practice far less because
10
+ * most route/minute combinations have zero traffic. The reader
11
+ * (`/stats/api-delivery`) folds the last 1440 minute buckets into
12
+ * four scalars; the writer does one atomic UPSERT per request via
13
+ * raw SQL.
14
+ *
15
+ * ## Why p95 via histogram and not from raw samples
16
+ *
17
+ * Storing a histogram per bucket gives us percentile estimates with
18
+ * ~10% error vs ground truth while keeping the table tiny. The
19
+ * `LATENCY_BUCKETS` boundary list is log-scale (powers of 2 in
20
+ * milliseconds) so it covers ~1 ms heartbeats through multi-second
21
+ * outliers in 12 columns. p95 falls out as a `findIndex` once the
22
+ * histogram is merged across all minute buckets in the window.
23
+ *
24
+ * ## Why fire-and-forget and not awaited
25
+ *
26
+ * Metric recording MUST NOT slow the user-facing response. The
27
+ * `record()` call dispatches the UPSERT to a `void`-discarded promise
28
+ * so a slow DB round-trip never blocks the request that produced
29
+ * the metric. Errors are swallowed at the source — losing a metric
30
+ * sample is strictly preferable to surfacing a metric-pipeline error
31
+ * to the user.
32
+ *
33
+ * ## Why we self-exempt the dashboard polling routes
34
+ *
35
+ * The dashboard refetches `/stats` and `/stats/api-delivery` on a
36
+ * timer. If we counted those requests they'd dominate the volume
37
+ * tile and inflate the error rate (a logged-out tab would hit
38
+ * `/stats` and get 401 repeatedly). The route-normaliser excludes
39
+ * them explicitly — see `shouldRecord`.
40
+ */
41
+ import { createLogger } from './logger.js';
42
+ import { getDB } from '../db.js';
43
+ const logger = createLogger('api-metrics');
44
+ // ─── Public constants ──────────────────────────────────────────────────────
45
+ /**
46
+ * Upper bound (exclusive) of each histogram bucket, in milliseconds.
47
+ * The last bucket is `>= 1024 ms` (no upper bound).
48
+ *
49
+ * Indexed:
50
+ * 0: <1ms 1: 1-2ms 2: 2-4ms 3: 4-8ms
51
+ * 4: 8-16ms 5: 16-32ms 6: 32-64ms 7: 64-128ms
52
+ * 8: 128-256 9: 256-512 10: 512-1024 11: >=1024
53
+ */
54
+ export const LATENCY_BUCKETS = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024];
55
+ export const HISTOGRAM_LENGTH = LATENCY_BUCKETS.length + 1; // 12
56
+ // ─── Pure helpers ──────────────────────────────────────────────────────────
57
+ /**
58
+ * Bucket a status code into one of 5 families. We keep the 1xx family
59
+ * out of the public type because no CMS route ever responds with one;
60
+ * if it ever happens, the `other` bucket catches it instead of
61
+ * silently being lost.
62
+ */
63
+ export function statusBucketOf(status) {
64
+ if (status >= 200 && status < 300)
65
+ return '2xx';
66
+ if (status >= 300 && status < 400)
67
+ return '3xx';
68
+ if (status >= 400 && status < 500)
69
+ return '4xx';
70
+ if (status >= 500 && status < 600)
71
+ return '5xx';
72
+ return 'other';
73
+ }
74
+ /**
75
+ * Return the index (0..HISTOGRAM_LENGTH-1) of the histogram bucket
76
+ * that this latency falls into. NaN / negative values fall in bucket
77
+ * 0 (treated as "<1ms") so a metric write never crashes on a bad
78
+ * input.
79
+ */
80
+ export function histogramBucketIndex(durationMs) {
81
+ if (!Number.isFinite(durationMs) || durationMs < 1)
82
+ return 0;
83
+ for (let i = 0; i < LATENCY_BUCKETS.length; i++) {
84
+ if (durationMs < LATENCY_BUCKETS[i])
85
+ return i;
86
+ }
87
+ return LATENCY_BUCKETS.length; // overflow bucket
88
+ }
89
+ /**
90
+ * Build a fresh histogram array with a single sample placed in the
91
+ * correct bucket. Used to produce the JSON payload for an INSERT.
92
+ */
93
+ export function makeHistogramFor(durationMs) {
94
+ const arr = new Array(HISTOGRAM_LENGTH).fill(0);
95
+ arr[histogramBucketIndex(durationMs)] = 1;
96
+ return arr;
97
+ }
98
+ /**
99
+ * Element-wise add two histograms. Tolerant of either operand being
100
+ * missing or shorter than `HISTOGRAM_LENGTH` — defends against schema
101
+ * mismatches (e.g. an old row written before a bucket boundary change).
102
+ */
103
+ export function mergeHistograms(a, b) {
104
+ const out = new Array(HISTOGRAM_LENGTH).fill(0);
105
+ for (let i = 0; i < HISTOGRAM_LENGTH; i++) {
106
+ out[i] = (a?.[i] ?? 0) + (b[i] ?? 0);
107
+ }
108
+ return out;
109
+ }
110
+ /**
111
+ * Snap a Date to the start of its UTC minute. Returns a new Date —
112
+ * never mutates the input.
113
+ */
114
+ export function bucketStartFor(now = new Date()) {
115
+ const ms = typeof now === 'number' ? now : now.getTime();
116
+ return new Date(ms - (ms % 60_000));
117
+ }
118
+ /**
119
+ * Estimate percentile-N from a merged histogram. Returns the upper
120
+ * bound of the bucket that contains the requested percentile —
121
+ * which is the right "user-visible" answer (we are happy to say
122
+ * "p95 < 256ms" rather than guess the exact value inside the
123
+ * 128-256 bucket).
124
+ *
125
+ * For the overflow bucket (no upper bound), we report
126
+ * `LATENCY_BUCKETS[last]` as a lower-bound estimate; callers
127
+ * displaying this should annotate with a "≥" prefix if they want
128
+ * to expose the open-ended nature.
129
+ */
130
+ export function estimatePercentile(histogram, percentile) {
131
+ const total = histogram.reduce((s, v) => s + v, 0);
132
+ if (total === 0)
133
+ return 0;
134
+ const target = total * percentile;
135
+ let acc = 0;
136
+ for (let i = 0; i < HISTOGRAM_LENGTH; i++) {
137
+ acc += histogram[i] ?? 0;
138
+ if (acc >= target) {
139
+ if (i < LATENCY_BUCKETS.length)
140
+ return LATENCY_BUCKETS[i];
141
+ return LATENCY_BUCKETS[LATENCY_BUCKETS.length - 1] * 2; // overflow estimate
142
+ }
143
+ }
144
+ return 0;
145
+ }
146
+ /**
147
+ * Normalise a request path to bound metric-row cardinality.
148
+ *
149
+ * /collections/posts/abc123 → /collections/posts/:id
150
+ * /globals/site-settings → /globals/:slug
151
+ * /collections/posts/abc123/restore→ /collections/posts/:id/restore
152
+ * /unknown/long/path/that/leaks → /other
153
+ *
154
+ * The "/other" bucket catches anything we don't explicitly know
155
+ * about so a single hostile client with random URLs can't blow up
156
+ * the table.
157
+ */
158
+ export function normalizeRoute(pathname) {
159
+ // Strip query string + trailing slash (except root).
160
+ let p = pathname.split('?')[0] ?? '/';
161
+ if (p.length > 1 && p.endsWith('/'))
162
+ p = p.slice(0, -1);
163
+ // Strip the `/api/cms` prefix if still present — depending on where
164
+ // we instrument, the path may or may not have been rewritten yet.
165
+ if (p.startsWith('/api/cms'))
166
+ p = p.slice('/api/cms'.length) || '/';
167
+ // Static or top-level routes — pass through verbatim.
168
+ if (KNOWN_STATIC_ROUTES.has(p))
169
+ return p;
170
+ // Pattern matchers for cardinality-bounding.
171
+ for (const { pattern, replacement } of ROUTE_PATTERNS) {
172
+ if (pattern.test(p))
173
+ return p.replace(pattern, replacement);
174
+ }
175
+ return '/other';
176
+ }
177
+ /**
178
+ * Routes that the dashboard polls itself, plus webhooks the cron
179
+ * fires. Counting these would either (a) cause feedback loops
180
+ * inflating the volume tile or (b) report cron-only traffic the
181
+ * editor never produces. The instrumentation skips them.
182
+ */
183
+ export function shouldRecord(normalizedRoute) {
184
+ return !SKIP_ROUTES.has(normalizedRoute);
185
+ }
186
+ // ─── Lookup tables (kept module-scope so they're built once) ───────────────
187
+ /**
188
+ * Verbatim routes — the dashboard, auth, health, etc. Keep this
189
+ * small; if your CMS exposes many top-level routes, prefer
190
+ * `ROUTE_PATTERNS` so the table stays bounded.
191
+ */
192
+ const KNOWN_STATIC_ROUTES = new Set([
193
+ '/',
194
+ '/health',
195
+ '/stats',
196
+ '/stats/api-delivery',
197
+ '/stats/content-health',
198
+ '/auth/login',
199
+ '/auth/logout',
200
+ '/auth/me',
201
+ '/auth/csrf',
202
+ '/auth/forgot-password',
203
+ '/auth/reset-password',
204
+ '/auth/totp/login',
205
+ '/setup/status',
206
+ '/setup/create-admin',
207
+ '/search',
208
+ '/users',
209
+ '/api-keys',
210
+ '/notifications',
211
+ '/notifications/stream',
212
+ '/notifications/read-all',
213
+ '/media',
214
+ '/media/upload',
215
+ '/forms',
216
+ '/seo/config',
217
+ '/seo/analyze',
218
+ '/redirects',
219
+ '/script-tags',
220
+ '/saved-sections',
221
+ '/page-templates',
222
+ '/webhooks',
223
+ '/webhooks/test',
224
+ ]);
225
+ /**
226
+ * Skip these from the metric — they're either the dashboard polling
227
+ * itself (feedback loop) or cron-only traffic that distorts the
228
+ * editor-facing tiles.
229
+ */
230
+ const SKIP_ROUTES = new Set([
231
+ '/stats',
232
+ '/stats/api-delivery',
233
+ '/stats/content-health',
234
+ '/health',
235
+ '/notifications/stream', // long-lived SSE; counting once on open is misleading
236
+ ]);
237
+ const ROUTE_PATTERNS = [
238
+ {
239
+ pattern: /^\/collections\/[^/]+\/[^/]+\/restore$/,
240
+ replacement: '/collections/:slug/:id/restore',
241
+ },
242
+ {
243
+ pattern: /^\/collections\/[^/]+\/[^/]+\/versions(?:\/[^/]+)?$/,
244
+ replacement: '/collections/:slug/:id/versions',
245
+ },
246
+ {
247
+ pattern: /^\/collections\/[^/]+\/[^/]+\/duplicate$/,
248
+ replacement: '/collections/:slug/:id/duplicate',
249
+ },
250
+ { pattern: /^\/collections\/[^/]+\/[^/]+$/, replacement: '/collections/:slug/:id' },
251
+ { pattern: /^\/collections\/[^/]+$/, replacement: '/collections/:slug' },
252
+ { pattern: /^\/globals\/[^/]+$/, replacement: '/globals/:slug' },
253
+ { pattern: /^\/media\/[^/]+$/, replacement: '/media/:id' },
254
+ { pattern: /^\/forms\/[^/]+\/submit$/, replacement: '/forms/:id/submit' },
255
+ { pattern: /^\/forms\/[^/]+\/submissions(?:\/[^/]+)?$/, replacement: '/forms/:id/submissions' },
256
+ { pattern: /^\/forms\/[^/]+$/, replacement: '/forms/:id' },
257
+ { pattern: /^\/users\/[^/]+$/, replacement: '/users/:id' },
258
+ { pattern: /^\/api-keys\/[^/]+$/, replacement: '/api-keys/:id' },
259
+ { pattern: /^\/redirects\/[^/]+$/, replacement: '/redirects/:id' },
260
+ { pattern: /^\/script-tags\/[^/]+$/, replacement: '/script-tags/:id' },
261
+ { pattern: /^\/saved-sections\/[^/]+$/, replacement: '/saved-sections/:id' },
262
+ { pattern: /^\/page-templates\/[^/]+$/, replacement: '/page-templates/:id' },
263
+ { pattern: /^\/webhooks\/[^/]+(?:\/.+)?$/, replacement: '/webhooks/:id' },
264
+ { pattern: /^\/notifications\/[^/]+\/read$/, replacement: '/notifications/:id/read' },
265
+ { pattern: /^\/cron\/.+/, replacement: '/cron/:job' },
266
+ { pattern: /^\/dev\/.+/, replacement: '/dev/:fn' },
267
+ { pattern: /^\/seo\/.+/, replacement: '/seo/:fn' },
268
+ { pattern: /^\/share\/.+/, replacement: '/share/:token' },
269
+ { pattern: /^\/preview\/.+/, replacement: '/preview/:fn' },
270
+ { pattern: /^\/page-builder\/.+/, replacement: '/page-builder/:fn' },
271
+ { pattern: /^\/ai\/.+/, replacement: '/ai/:fn' },
272
+ { pattern: /^\/auth\/.+/, replacement: '/auth/:fn' },
273
+ { pattern: /^\/resolve(?:\/.+)?$/, replacement: '/resolve' },
274
+ ];
275
+ // ─── Writer ────────────────────────────────────────────────────────────────
276
+ /**
277
+ * Record one API request to the metrics table. **Fire-and-forget** —
278
+ * callers MUST NOT `await` this. Errors are swallowed; the next
279
+ * request retries, and 1-minute aggregates are robust to occasional
280
+ * write loss.
281
+ *
282
+ * Writes a single SQL UPSERT so two concurrent requests landing in
283
+ * the same bucket interleave cleanly via Postgres's MVCC + the
284
+ * unique-on-conflict path. The histogram column is merged
285
+ * element-wise inside SQL, so we never read-modify-write from JS.
286
+ */
287
+ export function recordApiRequest(sample) {
288
+ const route = normalizeRoute(sample.pathname);
289
+ if (!shouldRecord(route))
290
+ return;
291
+ // Schedule the write but do not await. Returning `void` is the
292
+ // contract — callers shouldn't be tempted to `.then()` on it.
293
+ void writeMetricBucket({
294
+ bucketStart: bucketStartFor(),
295
+ route,
296
+ statusBucket: statusBucketOf(sample.status),
297
+ durationMs: sample.durationMs,
298
+ });
299
+ }
300
+ async function writeMetricBucket(args) {
301
+ let db;
302
+ try {
303
+ db = getDB();
304
+ }
305
+ catch {
306
+ // DB not initialised yet (e.g. dev server hot-reload race) —
307
+ // skip silently. There's nothing useful to log here and we
308
+ // don't want a stack trace on every dropped sample.
309
+ return;
310
+ }
311
+ // Some hosts ship a Prisma client that doesn't include the
312
+ // `apiRequestMetric` model (e.g. third-party deploys that copied
313
+ // an older schema). Guard so we degrade silently rather than 500ing
314
+ // every request.
315
+ if (!db || typeof db.$executeRawUnsafe !== 'function' || !('apiRequestMetric' in db)) {
316
+ return;
317
+ }
318
+ const histogram = makeHistogramFor(args.durationMs);
319
+ try {
320
+ // Atomic INSERT...ON CONFLICT DO UPDATE. The histogram merge is
321
+ // expressed as a `jsonb_build_array` over the existing + new
322
+ // bucket counts so we never read the prior row from JS.
323
+ //
324
+ // CUID-ish ID generation via gen_random_uuid() keeps the ID
325
+ // schema-compatible with the model's `@default(cuid())` (both
326
+ // are 26+ char ASCII strings; Prisma's cuid is preferred but
327
+ // SQL-level UUIDs are accepted by the column type and not
328
+ // surfaced to consumers).
329
+ await db.$executeRawUnsafe(`
330
+ INSERT INTO "actuate_api_request_metrics"
331
+ ("id", "bucketStart", "route", "statusBucket", "count", "sumLatencyMs", "maxLatencyMs", "latencyHistogram", "updatedAt")
332
+ VALUES (
333
+ 'm_' || replace(gen_random_uuid()::text, '-', ''),
334
+ $1::timestamp,
335
+ $2,
336
+ $3,
337
+ 1,
338
+ $4,
339
+ $4,
340
+ $5::jsonb,
341
+ NOW()
342
+ )
343
+ ON CONFLICT ("bucketStart", "route", "statusBucket") DO UPDATE
344
+ SET "count" = "actuate_api_request_metrics"."count" + 1,
345
+ "sumLatencyMs" = "actuate_api_request_metrics"."sumLatencyMs" + EXCLUDED."sumLatencyMs",
346
+ "maxLatencyMs" = GREATEST("actuate_api_request_metrics"."maxLatencyMs", EXCLUDED."maxLatencyMs"),
347
+ "latencyHistogram" = (
348
+ SELECT jsonb_agg(
349
+ COALESCE(("actuate_api_request_metrics"."latencyHistogram"->>i)::int, 0) +
350
+ COALESCE((EXCLUDED."latencyHistogram"->>i)::int, 0)
351
+ ORDER BY i
352
+ )
353
+ FROM generate_series(0, ${HISTOGRAM_LENGTH - 1}) AS i
354
+ ),
355
+ "updatedAt" = NOW()
356
+ `, args.bucketStart, args.route, args.statusBucket, args.durationMs, JSON.stringify(histogram));
357
+ }
358
+ catch (err) {
359
+ // Throttle the log so a misconfigured DB doesn't drown the
360
+ // logger. We rate-limit *log lines*, not metric writes — every
361
+ // request still attempts a write, but only one error/minute
362
+ // surfaces in console.
363
+ rateLimitedWarn('recordApiRequest write failed', err instanceof Error ? err.message : String(err));
364
+ }
365
+ }
366
+ let lastWarnAt = 0;
367
+ function rateLimitedWarn(msg, detail) {
368
+ const now = Date.now();
369
+ if (now - lastWarnAt < 60_000)
370
+ return;
371
+ lastWarnAt = now;
372
+ logger.warn(msg, { detail });
373
+ }
374
+ //# sourceMappingURL=api-metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-metrics.js","sourceRoot":"","sources":["../../src/diagnostics/api-metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sections.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sections.test.d.ts","sourceRoot":"","sources":["../../../src/sections/__tests__/sections.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,215 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { SECTION_TYPES, SECTION_TYPE_LIST, getSectionType, getSectionTypesForScope, isKnownSectionType, createSection, buildSection, addSection, removeSection, duplicateSection, toggleSectionVisibility, setSectionVisibility, moveSection, reorderSections, updateSectionContent, updateSectionSettings, updateSectionMeta, coerceSections, validateSectionContent, defaultPostHeader, coercePostHeader, createPostTemplate, coercePostTemplate, } from '../index.js';
3
+ const ALL_IDS = [
4
+ 'hero',
5
+ 'text',
6
+ 'feature',
7
+ 'stats',
8
+ 'cta',
9
+ 'article-body',
10
+ 'quote',
11
+ 'author-bio',
12
+ 'related-posts',
13
+ 'gallery',
14
+ ];
15
+ describe('section registry', () => {
16
+ it('lists every renderable type with a scope, defaults + fields', () => {
17
+ const ids = new Set(ALL_IDS);
18
+ expect(SECTION_TYPE_LIST).toHaveLength(ALL_IDS.length);
19
+ for (const def of SECTION_TYPE_LIST) {
20
+ expect(ids.has(def.id)).toBe(true);
21
+ expect(def.fields.length).toBeGreaterThan(0);
22
+ expect(def.defaultContent).toBeTruthy();
23
+ expect(def.scope.length).toBeGreaterThan(0);
24
+ }
25
+ });
26
+ it('getSectionType / isKnownSectionType reflect the registry', () => {
27
+ expect(getSectionType('hero')).toBe(SECTION_TYPES.hero);
28
+ expect(getSectionType('nope')).toBeUndefined();
29
+ expect(isKnownSectionType('cta')).toBe(true);
30
+ expect(isKnownSectionType('article-body')).toBe(true);
31
+ expect(isKnownSectionType('nope')).toBe(false);
32
+ });
33
+ });
34
+ describe('getSectionTypesForScope', () => {
35
+ it('page scope excludes post-only types', () => {
36
+ const pageIds = getSectionTypesForScope('page').map((d) => d.id);
37
+ expect(pageIds).toEqual(['hero', 'text', 'feature', 'stats', 'cta']);
38
+ expect(pageIds).not.toContain('article-body');
39
+ });
40
+ it('post scope includes shared + post-only types', () => {
41
+ const postIds = getSectionTypesForScope('post').map((d) => d.id);
42
+ // shared types are usable on both surfaces
43
+ expect(postIds).toContain('hero');
44
+ // post-only types appear
45
+ for (const id of ['article-body', 'quote', 'author-bio', 'related-posts', 'gallery']) {
46
+ expect(postIds).toContain(id);
47
+ }
48
+ });
49
+ it('every type is reachable from at least one scope', () => {
50
+ const reachable = new Set([
51
+ ...getSectionTypesForScope('page').map((d) => d.id),
52
+ ...getSectionTypesForScope('post').map((d) => d.id),
53
+ ]);
54
+ for (const id of ALL_IDS)
55
+ expect(reachable.has(id)).toBe(true);
56
+ });
57
+ });
58
+ describe('createSection / buildSection', () => {
59
+ it('seeds registry defaults with a unique id and deep-cloned content', () => {
60
+ const a = createSection('stats');
61
+ const b = createSection('stats');
62
+ expect(a.id).not.toBe(b.id);
63
+ expect(a.sectionType).toBe('stats');
64
+ expect(a.name).toBe(SECTION_TYPES.stats.name);
65
+ a.content.stats.push({ value: 'x' });
66
+ expect(b.content.stats.length).toBe(SECTION_TYPES.stats.defaultContent.stats.length);
67
+ });
68
+ it('buildSection merges overrides over the defaults', () => {
69
+ const s = buildSection('hero', {
70
+ name: 'Custom hero',
71
+ content: { heading: 'Override' },
72
+ settings: { paddingY: 'sm' },
73
+ });
74
+ expect(s.name).toBe('Custom hero');
75
+ expect(s.content.heading).toBe('Override');
76
+ // untouched default keys survive the merge
77
+ expect(s.content.primaryButtonLabel).toBe(SECTION_TYPES.hero.defaultContent.primaryButtonLabel);
78
+ expect(s.settings.paddingY).toBe('sm');
79
+ });
80
+ });
81
+ describe('array helpers', () => {
82
+ const s1 = createSection('hero');
83
+ const s2 = createSection('text');
84
+ const s3 = createSection('cta');
85
+ const list = [s1, s2, s3];
86
+ it('addSection appends by default and inserts at an index', () => {
87
+ const extra = createSection('stats');
88
+ expect(addSection(list, extra).map((s) => s.id)).toEqual([s1.id, s2.id, s3.id, extra.id]);
89
+ expect(addSection(list, extra, 1).map((s) => s.id)).toEqual([s1.id, extra.id, s2.id, s3.id]);
90
+ });
91
+ it('removeSection / moveSection / reorderSections', () => {
92
+ expect(removeSection(list, s2.id).map((s) => s.id)).toEqual([s1.id, s3.id]);
93
+ expect(moveSection(list, s1.id, 99).map((s) => s.id)).toEqual([s2.id, s3.id, s1.id]);
94
+ expect(reorderSections(list, [s3.id]).map((s) => s.id)).toEqual([s3.id, s1.id, s2.id]);
95
+ });
96
+ it('duplicateSection inserts a new-id copy right after the original', () => {
97
+ const out = duplicateSection(list, s2.id);
98
+ expect(out).toHaveLength(4);
99
+ expect(out[2].id).not.toBe(s2.id);
100
+ expect(out[2].name).toBe(`${s2.name} (Copy)`);
101
+ });
102
+ it('visibility + patch helpers are immutable', () => {
103
+ expect(toggleSectionVisibility(list, s1.id).find((s) => s.id === s1.id).visible).toBe(false);
104
+ expect(setSectionVisibility(list, s1.id, false).find((s) => s.id === s1.id).visible).toBe(false);
105
+ expect(updateSectionContent(list, s1.id, { heading: 'New' }).find((s) => s.id === s1.id).content
106
+ .heading).toBe('New');
107
+ expect(list.find((s) => s.id === s1.id).content.heading).not.toBe('New');
108
+ expect(updateSectionSettings(list, s1.id, { paddingY: 'sm' }).find((s) => s.id === s1.id).settings
109
+ .paddingY).toBe('sm');
110
+ expect(updateSectionMeta(list, s1.id, { name: 'Renamed' }).find((s) => s.id === s1.id).name).toBe('Renamed');
111
+ });
112
+ });
113
+ describe('coerceSections', () => {
114
+ it('drops unknown types and backfills ids/names/visibility', () => {
115
+ const out = coerceSections([
116
+ { sectionType: 'hero', content: { heading: 'Hi' } },
117
+ { sectionType: 'ghost' }, // unknown — dropped
118
+ { sectionType: 'text', visible: false },
119
+ 'garbage',
120
+ null,
121
+ ]);
122
+ expect(out).toHaveLength(2);
123
+ expect(out[0].sectionType).toBe('hero');
124
+ expect(out[0].id).toBeTruthy();
125
+ expect(out[0].name).toBe(SECTION_TYPES.hero.name);
126
+ expect(out[1].sectionType).toBe('text');
127
+ expect(out[1].visible).toBe(false);
128
+ });
129
+ it('returns an empty array for non-array input', () => {
130
+ expect(coerceSections(undefined)).toEqual([]);
131
+ expect(coerceSections({})).toEqual([]);
132
+ });
133
+ });
134
+ describe('validateSectionContent', () => {
135
+ it('flags an unknown section type as a hard error', () => {
136
+ const r = validateSectionContent('ghost', {});
137
+ expect(r.valid).toBe(false);
138
+ expect(r.errors[0].message).toMatch(/Unknown section type/);
139
+ });
140
+ it('flags missing required fields', () => {
141
+ const r = validateSectionContent('hero', { heading: '' });
142
+ expect(r.valid).toBe(false);
143
+ expect(r.errors.some((e) => e.field === 'heading')).toBe(true);
144
+ });
145
+ it('flags invalid button URLs and missing image alt text', () => {
146
+ const r = validateSectionContent('feature', {
147
+ heading: 'Has heading',
148
+ buttonLabel: 'Learn more',
149
+ buttonUrl: 'not a url',
150
+ imageUrl: 'https://x/img.png',
151
+ imageAlt: '',
152
+ });
153
+ expect(r.valid).toBe(false);
154
+ expect(r.errors.some((e) => e.message.includes('not a valid URL'))).toBe(true);
155
+ expect(r.errors.some((e) => e.field === 'imageAlt')).toBe(true);
156
+ });
157
+ it('warns when a URL is set without its button label', () => {
158
+ const r = validateSectionContent('hero', {
159
+ heading: 'Title',
160
+ primaryButtonUrl: '/go',
161
+ primaryButtonLabel: '',
162
+ });
163
+ expect(r.valid).toBe(true);
164
+ expect(r.warnings.some((w) => w.field === 'primaryButtonLabel')).toBe(true);
165
+ });
166
+ it('passes a well-formed section', () => {
167
+ const r = validateSectionContent('hero', createSection('hero').content);
168
+ expect(r.valid).toBe(true);
169
+ expect(r.errors).toEqual([]);
170
+ });
171
+ it('validates post-scoped types: quote requires its quote field', () => {
172
+ expect(validateSectionContent('quote', { quote: '' }).valid).toBe(false);
173
+ expect(validateSectionContent('quote', createSection('quote').content).valid).toBe(true);
174
+ });
175
+ it('warns when a gallery image is missing alt text', () => {
176
+ const r = validateSectionContent('gallery', {
177
+ images: [{ url: 'https://x/a.png', alt: '' }],
178
+ });
179
+ expect(r.valid).toBe(true);
180
+ expect(r.warnings.some((w) => w.field === 'images')).toBe(true);
181
+ });
182
+ it('accepts a default article-body section', () => {
183
+ const r = validateSectionContent('article-body', createSection('article-body').content);
184
+ expect(r.valid).toBe(true);
185
+ });
186
+ });
187
+ describe('post template helpers', () => {
188
+ it('defaultPostHeader is a full centered config', () => {
189
+ const h = defaultPostHeader();
190
+ expect(h.layout).toBe('centered');
191
+ expect(h.showFeaturedImage).toBe(true);
192
+ });
193
+ it('coercePostHeader fills missing keys and rejects bad layout', () => {
194
+ const h = coercePostHeader({ layout: 'sideways', showAuthor: false });
195
+ expect(h.layout).toBe('centered'); // invalid → default
196
+ expect(h.showAuthor).toBe(false); // explicit override kept
197
+ expect(h.showDate).toBe(true); // missing → default
198
+ });
199
+ it('createPostTemplate seeds a usable default template', () => {
200
+ const t = createPostTemplate('blog');
201
+ expect(t.postType).toBe('blog');
202
+ expect(t.sections.map((s) => s.sectionType)).toEqual(['article-body', 'related-posts']);
203
+ expect(t.sections[0].id).not.toBe(t.sections[1].id);
204
+ });
205
+ it('coercePostTemplate normalizes header + drops unknown sections', () => {
206
+ const t = coercePostTemplate('news', {
207
+ header: { layout: 'overlay' },
208
+ sections: [{ sectionType: 'quote', content: { quote: 'Hi' } }, { sectionType: 'ghost' }],
209
+ });
210
+ expect(t.header.layout).toBe('overlay');
211
+ expect(t.sections).toHaveLength(1);
212
+ expect(t.sections[0].sectionType).toBe('quote');
213
+ });
214
+ });
215
+ //# sourceMappingURL=sections.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sections.test.js","sourceRoot":"","sources":["../../../src/sections/__tests__/sections.test.ts"],"names":[],"mappings":"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"}