@beechcms/core 0.4.0 → 0.4.2

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/auth/password-reset-token.repository.d.ts +0 -1
  2. package/dist/auth/password-reset-token.repository.d.ts.map +1 -1
  3. package/dist/automations-grammar-words.d.ts +2 -0
  4. package/dist/automations-grammar-words.d.ts.map +1 -0
  5. package/dist/automations-grammar-words.js +6 -0
  6. package/dist/automations.repository.interface.d.ts +19 -0
  7. package/dist/automations.repository.interface.d.ts.map +1 -0
  8. package/dist/automations.repository.interface.js +1 -0
  9. package/dist/automations.runner.interface.d.ts +10 -0
  10. package/dist/automations.runner.interface.d.ts.map +1 -0
  11. package/dist/automations.runner.interface.js +1 -0
  12. package/dist/automations.runner.stub.d.ts +5 -0
  13. package/dist/automations.runner.stub.d.ts.map +1 -0
  14. package/dist/automations.runner.stub.js +5 -0
  15. package/dist/automations.types.d.ts +73 -0
  16. package/dist/automations.types.d.ts.map +1 -0
  17. package/dist/automations.types.js +1 -0
  18. package/dist/clock.d.ts +25 -0
  19. package/dist/clock.d.ts.map +1 -0
  20. package/dist/clock.js +9 -0
  21. package/dist/content-scan.repository.d.ts +9 -0
  22. package/dist/content-scan.repository.d.ts.map +1 -0
  23. package/dist/content-scan.repository.js +1 -0
  24. package/dist/engine.d.ts +24 -24
  25. package/dist/engine.js +22 -22
  26. package/dist/id-generator.d.ts +19 -0
  27. package/dist/id-generator.d.ts.map +1 -0
  28. package/dist/id-generator.js +7 -0
  29. package/dist/index.d.ts +27 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +27 -3
  32. package/dist/notifications/notification-service.d.ts +29 -0
  33. package/dist/notifications/notification-service.d.ts.map +1 -0
  34. package/dist/notifications/notification-service.js +1 -0
  35. package/dist/notifications/notification.repository.d.ts +51 -0
  36. package/dist/notifications/notification.repository.d.ts.map +1 -0
  37. package/dist/notifications/notification.repository.js +10 -0
  38. package/dist/observability/activity-log.repository.d.ts +58 -0
  39. package/dist/observability/activity-log.repository.d.ts.map +1 -0
  40. package/dist/observability/activity-log.repository.js +1 -0
  41. package/dist/observability/activity-logger.d.ts +49 -0
  42. package/dist/observability/activity-logger.d.ts.map +1 -0
  43. package/dist/observability/activity-logger.js +12 -0
  44. package/dist/observability/analytics.repository.d.ts +37 -0
  45. package/dist/observability/analytics.repository.d.ts.map +1 -0
  46. package/dist/observability/analytics.repository.js +1 -0
  47. package/dist/scheduler.interface.d.ts +4 -0
  48. package/dist/scheduler.interface.d.ts.map +1 -0
  49. package/dist/scheduler.interface.js +1 -0
  50. package/dist/scheduler.stub.d.ts +5 -0
  51. package/dist/scheduler.stub.d.ts.map +1 -0
  52. package/dist/scheduler.stub.js +5 -0
  53. package/dist/search/search.repository.d.ts +45 -0
  54. package/dist/search/search.repository.d.ts.map +1 -0
  55. package/dist/search/search.repository.js +1 -0
  56. package/dist/seed-registry.d.ts +49 -0
  57. package/dist/seed-registry.d.ts.map +1 -0
  58. package/dist/seed-registry.js +38 -0
  59. package/dist/slug-utils.d.ts.map +1 -1
  60. package/dist/slug-utils.js +4 -3
  61. package/dist/types.d.ts +1 -1
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/validation.d.ts +7 -7
  64. package/dist/validation.d.ts.map +1 -1
  65. package/dist/validation.js +29 -106
  66. package/dist/widget/widget.repository.d.ts +113 -0
  67. package/dist/widget/widget.repository.d.ts.map +1 -0
  68. package/dist/widget/widget.repository.js +1 -0
  69. package/package.json +9 -3
@@ -227,6 +227,7 @@ function requiredAliases(seed, operation) {
227
227
  }
228
228
  function buildBranchSchema(branch, options) {
229
229
  const nullable = options.allowNull ? z.null() : null;
230
+ const preprocessEmpty = (schema) => z.preprocess((val) => (val === '' || val === null ? (options.allowNull ? null : undefined) : val), schema.optional());
230
231
  switch (branch.type) {
231
232
  case 'text': {
232
233
  const schema = stringSchema
@@ -265,56 +266,48 @@ function buildBranchSchema(branch, options) {
265
266
  return nullable ? z.union([schema, nullable]) : schema;
266
267
  }
267
268
  case 'number': {
268
- const schema = finiteNumberSchema;
269
+ const schema = preprocessEmpty(finiteNumberSchema);
269
270
  return nullable ? z.union([schema, nullable]) : schema;
270
271
  }
271
272
  case 'boolean': {
272
- const schema = booleanSchema;
273
+ const schema = preprocessEmpty(booleanSchema);
273
274
  return nullable ? z.union([schema, nullable]) : schema;
274
275
  }
275
276
  case 'date': {
276
- const schema = stringSchema
277
+ const schema = preprocessEmpty(stringSchema
277
278
  .transform((value) => sanitizePlainString(value))
278
- .refine((value) => isIsoDateString(value), { message: 'Expected date(ISO)' });
279
+ .refine((value) => isIsoDateString(value), { message: 'Expected date(ISO)' }));
279
280
  return nullable ? z.union([schema, nullable]) : schema;
280
281
  }
281
282
  case 'json':
282
283
  case 'tags': {
283
- const schema = jsonObjectOrArraySchema;
284
+ const schema = preprocessEmpty(jsonObjectOrArraySchema);
284
285
  return nullable ? z.union([schema, nullable]) : schema;
285
286
  }
286
287
  case 'file': {
287
288
  if (isAssetListBranch(branch)) {
288
- const schema = z
289
- .any()
289
+ const schema = preprocessEmpty(z.any()
290
290
  .transform((rawValue, ctx) => {
291
291
  const normalized = normalizeAssetListValue(rawValue);
292
292
  if (!normalized) {
293
- ctx.addIssue({
294
- code: 'custom',
295
- message: 'Expected url-string[]',
296
- });
293
+ ctx.addIssue({ code: 'custom', message: 'Expected url-string[]' });
297
294
  return z.NEVER;
298
295
  }
299
296
  return normalized;
300
297
  })
301
- .pipe(z.array(z.url()));
298
+ .pipe(z.array(z.string().url())));
302
299
  return nullable ? z.union([schema, nullable]) : schema;
303
300
  }
304
- const schema = z
305
- .any()
301
+ const schema = preprocessEmpty(z.any()
306
302
  .transform((rawValue, ctx) => {
307
303
  const normalized = normalizeHttpUrl(rawValue);
308
304
  if (!normalized) {
309
- ctx.addIssue({
310
- code: 'custom',
311
- message: 'Expected url-string',
312
- });
305
+ ctx.addIssue({ code: 'custom', message: 'Expected url-string' });
313
306
  return z.NEVER;
314
307
  }
315
308
  return normalized;
316
309
  })
317
- .pipe(z.url());
310
+ .pipe(z.string().url()));
318
311
  return nullable ? z.union([schema, nullable]) : schema;
319
312
  }
320
313
  default:
@@ -371,95 +364,21 @@ function makeDetail(field, expected, received) {
371
364
  function isIsoDateString(value) {
372
365
  if (!/^\d{4}-\d{2}-\d{2}/.test(value))
373
366
  return false;
374
- return !Number.isNaN(Date.parse(value));
375
- }
376
- function validateBranchValue(branch, alias, rawValue, options) {
377
- if (rawValue === null) {
378
- return options.allowNull
379
- ? { ok: true, value: null }
380
- : { ok: false, detail: makeDetail(alias, branch.type, rawValue) };
381
- }
382
- switch (branch.type) {
383
- case 'text': {
384
- if (!stringSchema.safeParse(rawValue).success) {
385
- return { ok: false, detail: makeDetail(alias, 'string', rawValue) };
386
- }
387
- const sanitized = sanitizePlainString(rawValue);
388
- if (sanitized.length > options.maxTextLength) {
389
- return { ok: false, detail: makeDetail(alias, `string(max:${options.maxTextLength})`, rawValue) };
390
- }
391
- return { ok: true, value: sanitized };
392
- }
393
- case 'richtext': {
394
- const sanitized = sanitizeRichtext(rawValue);
395
- if (!sanitized.valid) {
396
- return { ok: false, detail: makeDetail(alias, 'richtext-json|string', rawValue) };
397
- }
398
- if (sanitized.size > options.maxTextLength) {
399
- return {
400
- ok: false,
401
- detail: makeDetail(alias, `richtext(max:${options.maxTextLength})`, rawValue),
402
- };
403
- }
404
- return { ok: true, value: sanitized.value, dangerous: sanitized.dangerous };
405
- }
406
- case 'number': {
407
- if (!finiteNumberSchema.safeParse(rawValue).success) {
408
- return { ok: false, detail: makeDetail(alias, 'number', rawValue) };
409
- }
410
- return { ok: true, value: rawValue };
411
- }
412
- case 'boolean': {
413
- if (!booleanSchema.safeParse(rawValue).success) {
414
- return { ok: false, detail: makeDetail(alias, 'boolean', rawValue) };
415
- }
416
- return { ok: true, value: rawValue };
417
- }
418
- case 'date': {
419
- if (!stringSchema.safeParse(rawValue).success) {
420
- return { ok: false, detail: makeDetail(alias, 'date(ISO)', rawValue) };
421
- }
422
- const value = sanitizePlainString(rawValue);
423
- if (!isIsoDateString(value)) {
424
- return { ok: false, detail: makeDetail(alias, 'date(ISO)', rawValue) };
425
- }
426
- return { ok: true, value };
427
- }
428
- case 'json':
429
- case 'tags': {
430
- const isValidJsonLike = (typeof rawValue === 'object' && rawValue !== null) || Array.isArray(rawValue);
431
- if (!isValidJsonLike || typeof rawValue === 'string') {
432
- return { ok: false, detail: makeDetail(alias, 'object|array', rawValue) };
433
- }
434
- return { ok: true, value: rawValue };
435
- }
436
- case 'file': {
437
- if (isAssetListBranch(branch)) {
438
- const listValue = normalizeAssetListValue(rawValue);
439
- if (!listValue) {
440
- return { ok: false, detail: makeDetail(alias, 'url-string[]', rawValue) };
441
- }
442
- return { ok: true, value: listValue };
443
- }
444
- const singleUrl = normalizeHttpUrl(rawValue);
445
- if (!singleUrl) {
446
- return { ok: false, detail: makeDetail(alias, 'url-string', rawValue) };
447
- }
448
- return { ok: true, value: singleUrl };
449
- }
450
- default:
451
- throw new Error(`Unhandled branch type: ${branch.type}`);
452
- }
367
+ const parsed = Date.parse(value);
368
+ if (Number.isNaN(parsed))
369
+ return false;
370
+ const d = new Date(parsed);
371
+ return d.toISOString().startsWith(value.substring(0, 10));
453
372
  }
454
373
  /**
455
- * Foundation comune per validazione e sanitizzazione payload schema-driven.
456
- * Usata da Public API e riusabile nel Botanical Engine.
374
+ * Common foundation for schema-driven validation and sanitization of payloads.
375
+ * Used by the Public API and reusable in the Botanical Engine.
457
376
  *
458
- * Ordine di esecuzione obbligatorio al momento della scrittura:
377
+ * Mandatory execution order at the time of writing:
459
378
  * validate raw → hash (privacy policy) → store
460
- * L'hashing avviene DOPO la validazione, non prima.
461
- * Questo garantisce che il valore validato sia il valore raw originale,
462
- * non il digest — evitando false failure su campi con formato (es. email).
379
+ * Hashing occurs AFTER validation, not before.
380
+ * This ensures that the validated value is the original raw value,
381
+ * not the digest — avoiding false failures on fields with formats (e.g., email).
463
382
  */
464
383
  export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
465
384
  const normalizedOptions = {
@@ -508,6 +427,9 @@ export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
508
427
  }
509
428
  continue;
510
429
  }
430
+ if (issue.message === 'Required' && normalizedOptions.enforceRequiredFields) {
431
+ continue; // Let enforceRequiredFields logic below handle it properly with detailed messages
432
+ }
511
433
  const field = String(issue.path[0] ?? 'payload');
512
434
  const receivedValue = filteredPayload[field];
513
435
  const expected = typeof issue.message === 'string' && issue.message.startsWith('Expected ')
@@ -535,7 +457,8 @@ export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
535
457
  });
536
458
  continue;
537
459
  }
538
- if (isMissingRequiredValue(data[branch.alias])) {
460
+ const valueToCheck = parsed.success ? data[branch.alias] : filteredPayload[branch.alias];
461
+ if (isMissingRequiredValue(valueToCheck)) {
539
462
  requiredFieldsMissing.push(branch.alias);
540
463
  details.push({
541
464
  field: branch.alias,
@@ -564,7 +487,7 @@ export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
564
487
  };
565
488
  }
566
489
  /**
567
- * Valida status content supportati dal CMS.
490
+ * Validates content status supported by the CMS.
568
491
  */
569
492
  export function isValidContentStatus(value) {
570
493
  return statusSchema.safeParse(value).success;
@@ -0,0 +1,113 @@
1
+ import type { Seed } from '../types.js';
2
+ /**
3
+ * Discriminated union describing the aggregate to compute over a content table.
4
+ *
5
+ * The widget routes accept this shape from the dashboard. Implementations are
6
+ * responsible for translating each variant into a safe SQL expression and must
7
+ * never interpolate the column or value fields without prior validation.
8
+ */
9
+ export type AggregateFormula = {
10
+ op: 'count';
11
+ } | {
12
+ op: 'sum';
13
+ column: string;
14
+ } | {
15
+ op: 'avg';
16
+ column: string;
17
+ } | {
18
+ op: 'min';
19
+ column: string;
20
+ } | {
21
+ op: 'max';
22
+ column: string;
23
+ } | {
24
+ op: 'countWhere';
25
+ column: string;
26
+ value: unknown;
27
+ } | {
28
+ op: 'percentageOf';
29
+ numeratorColumn: string;
30
+ denominatorColumn: string;
31
+ };
32
+ /**
33
+ * Time range applied as a WHERE filter for widget queries.
34
+ *
35
+ * "all" means no temporal restriction. The other values bracket the most recent
36
+ * 7 days, 1 month, and 1 year respectively, anchored on `created_at`.
37
+ */
38
+ export type TimeWindow = 'week' | 'month' | 'year' | 'all';
39
+ export interface LeaderboardEntry {
40
+ id: string;
41
+ label: string;
42
+ score: number | string;
43
+ }
44
+ export interface LeaderboardOptions {
45
+ scoreColumn: string;
46
+ limit: number;
47
+ orderDirection: 'ASC' | 'DESC';
48
+ }
49
+ export interface TimeseriesPoint {
50
+ label: string;
51
+ value: number;
52
+ }
53
+ export interface WidgetListFilter {
54
+ column: string;
55
+ op: string;
56
+ value: unknown;
57
+ }
58
+ export interface WidgetListOptions {
59
+ limit: number;
60
+ offset: number;
61
+ search?: string;
62
+ filters?: WidgetListFilter[];
63
+ orderByColumn?: string;
64
+ orderDirection?: 'ASC' | 'DESC';
65
+ }
66
+ export interface WidgetListResult {
67
+ entries: Array<Record<string, unknown>>;
68
+ totalCount: number;
69
+ }
70
+ export interface GrowthResult {
71
+ currentValue: number;
72
+ previousValue: number;
73
+ }
74
+ /**
75
+ * Read-only data access contract for widget routes.
76
+ *
77
+ * Implementations must validate every column alias derived from user input
78
+ * against the seed before composing SQL, and must bind every user-supplied
79
+ * value via parameterised statements. SQL keywords (ORDER direction, aggregate
80
+ * function names) must be selected via hardcoded branches, never interpolated.
81
+ */
82
+ export interface IWidgetRepository {
83
+ /**
84
+ * Returns the formula result for the given time window. Always returns a
85
+ * number; implementations must return 0 when the query produces no rows.
86
+ */
87
+ aggregate(seed: Seed, formula: AggregateFormula, window: TimeWindow): Promise<number>;
88
+ /**
89
+ * Evaluates the formula twice — once for the current window period and once
90
+ * for the equivalent previous period — to support trend calculations.
91
+ * Implementations must return { currentValue: 0, previousValue: 0 } on
92
+ * empty results.
93
+ */
94
+ growth(seed: Seed, formula: AggregateFormula, window: TimeWindow): Promise<GrowthResult>;
95
+ /**
96
+ * Returns entries sorted by scoreColumn, excluding nulls. label resolves
97
+ * from seed.displayNameAlias; falls back to id when not set.
98
+ */
99
+ leaderboard(seed: Seed, options: LeaderboardOptions): Promise<LeaderboardEntry[]>;
100
+ /**
101
+ * Paginated read of content entries. Filters and search are applied
102
+ * server-side. The caller is responsible for deserialising branch values
103
+ * from the raw Record.
104
+ */
105
+ list(seed: Seed, options: WidgetListOptions): Promise<WidgetListResult>;
106
+ /**
107
+ * Groups entries by a date bucket derived from groupColumn and aggregates
108
+ * the formula. Days with no entries are omitted (no zero-fill). Points are
109
+ * ordered ascending by label.
110
+ */
111
+ timeseries(seed: Seed, formula: AggregateFormula, window: TimeWindow, groupColumn: string): Promise<TimeseriesPoint[]>;
112
+ }
113
+ //# sourceMappingURL=widget.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget.repository.d.ts","sourceRoot":"","sources":["../../src/widget/widget.repository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -18,18 +18,24 @@
18
18
  "build": "tsc",
19
19
  "dev": "tsc -w --preserveWatchOutput",
20
20
  "lint": "eslint .",
21
- "type-check": "tsc --noEmit"
21
+ "type-check": "tsc --noEmit",
22
+ "test": "vitest run",
23
+ "test:coverage": "vitest run --coverage"
24
+ },
25
+ "devDependencies": {
26
+ "@vitest/coverage-v8": "^3.2.4",
27
+ "vitest": "^3.2.4"
22
28
  },
23
29
  "dependencies": {
24
30
  "@tiptap/core": "^3.22.3",
25
31
  "@tiptap/extension-highlight": "^3.22.3",
26
32
  "@tiptap/extension-image": "^3.22.3",
27
33
  "@tiptap/extension-link": "^3.22.3",
34
+ "@tiptap/extension-mathematics": "^3.22.3",
28
35
  "@tiptap/extension-subscript": "^3.22.3",
29
36
  "@tiptap/extension-superscript": "^3.22.3",
30
37
  "@tiptap/extension-table": "^3.22.3",
31
38
  "@tiptap/extension-text-align": "^3.22.3",
32
- "@tiptap/extension-mathematics": "^3.22.3",
33
39
  "@tiptap/html": "^3.22.3",
34
40
  "@tiptap/starter-kit": "^3.22.3",
35
41
  "katex": "^0.16.11",