@beechcms/core 0.4.0 → 0.4.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 (43) 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/clock.d.ts +25 -0
  4. package/dist/clock.d.ts.map +1 -0
  5. package/dist/clock.js +9 -0
  6. package/dist/content-scan.repository.d.ts +9 -0
  7. package/dist/content-scan.repository.d.ts.map +1 -0
  8. package/dist/content-scan.repository.js +1 -0
  9. package/dist/engine.d.ts +24 -24
  10. package/dist/engine.js +22 -22
  11. package/dist/id-generator.d.ts +19 -0
  12. package/dist/id-generator.d.ts.map +1 -0
  13. package/dist/id-generator.js +7 -0
  14. package/dist/index.d.ts +20 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +20 -3
  17. package/dist/notifications/notification-service.d.ts +29 -0
  18. package/dist/notifications/notification-service.d.ts.map +1 -0
  19. package/dist/notifications/notification-service.js +1 -0
  20. package/dist/notifications/notification.repository.d.ts +51 -0
  21. package/dist/notifications/notification.repository.d.ts.map +1 -0
  22. package/dist/notifications/notification.repository.js +10 -0
  23. package/dist/observability/activity-log.repository.d.ts +58 -0
  24. package/dist/observability/activity-log.repository.d.ts.map +1 -0
  25. package/dist/observability/activity-log.repository.js +1 -0
  26. package/dist/observability/activity-logger.d.ts +49 -0
  27. package/dist/observability/activity-logger.d.ts.map +1 -0
  28. package/dist/observability/activity-logger.js +12 -0
  29. package/dist/observability/analytics.repository.d.ts +37 -0
  30. package/dist/observability/analytics.repository.d.ts.map +1 -0
  31. package/dist/observability/analytics.repository.js +1 -0
  32. package/dist/search/search.repository.d.ts +45 -0
  33. package/dist/search/search.repository.d.ts.map +1 -0
  34. package/dist/search/search.repository.js +1 -0
  35. package/dist/seed-registry.d.ts +49 -0
  36. package/dist/seed-registry.d.ts.map +1 -0
  37. package/dist/seed-registry.js +29 -0
  38. package/dist/validation.d.ts +7 -7
  39. package/dist/validation.js +7 -7
  40. package/dist/widget/widget.repository.d.ts +113 -0
  41. package/dist/widget/widget.repository.d.ts.map +1 -0
  42. package/dist/widget/widget.repository.js +1 -0
  43. package/package.json +9 -3
@@ -1,5 +1,4 @@
1
1
  export interface NewPasswordResetToken {
2
- id: string;
3
2
  userId: string;
4
3
  tokenHash: string;
5
4
  expiresAt: number;
@@ -1 +1 @@
1
- {"version":3,"file":"password-reset-token.repository.d.ts","sourceRoot":"","sources":["../../src/auth/password-reset-token.repository.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEtE,0FAA0F;IAC1F,MAAM,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEpD;;;;OAIG;IACH,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAA;IAEtG,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/D"}
1
+ {"version":3,"file":"password-reset-token.repository.d.ts","sourceRoot":"","sources":["../../src/auth/password-reset-token.repository.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEtE,0FAA0F;IAC1F,MAAM,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEpD;;;;OAIG;IACH,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAA;IAEtG,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/D"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Time abstraction. Hides direct calls to {@link Date.now} so callers can
3
+ * swap in deterministic clocks during tests without resorting to global
4
+ * timer mocks (e.g. vi.useFakeTimers, sinon, monkey-patching Date).
5
+ */
6
+ export interface IClock {
7
+ /**
8
+ * Returns the current Unix timestamp in milliseconds.
9
+ * Equivalent to Date.now() in production.
10
+ * Overridable in tests for deterministic time-sensitive assertions.
11
+ */
12
+ now(): number;
13
+ /**
14
+ * Returns the current Unix timestamp in whole seconds.
15
+ * Equivalent to Math.floor(Date.now() / 1000) in production.
16
+ * Used by JWT issuance, session expiry, and analytics day-bucket computations.
17
+ */
18
+ nowSeconds(): number;
19
+ }
20
+ /**
21
+ * Production singleton. Delegates to the host runtime clock. Stateless and
22
+ * therefore safe to share across requests.
23
+ */
24
+ export declare const SystemClock: IClock;
25
+ //# sourceMappingURL=clock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACrB;;;;OAIG;IACH,GAAG,IAAI,MAAM,CAAA;IAEb;;;;OAIG;IACH,UAAU,IAAI,MAAM,CAAA;CACrB;AAID;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,MAGzB,CAAA"}
package/dist/clock.js ADDED
@@ -0,0 +1,9 @@
1
+ const MILLISECONDS_PER_SECOND = 1000;
2
+ /**
3
+ * Production singleton. Delegates to the host runtime clock. Stateless and
4
+ * therefore safe to share across requests.
5
+ */
6
+ export const SystemClock = {
7
+ now: () => Date.now(),
8
+ nowSeconds: () => Math.floor(Date.now() / MILLISECONDS_PER_SECOND),
9
+ };
@@ -0,0 +1,9 @@
1
+ import type { Seed } from './types.js';
2
+ export interface IContentScanRepository {
3
+ /**
4
+ * Scans across all registered seeds to identify media keys that are currently referenced
5
+ * by any content entry. Used for orphaned media detection and storage analytics.
6
+ */
7
+ getReferencedMediaKeys(seeds: Seed[]): Promise<Set<string>>;
8
+ }
9
+ //# sourceMappingURL=content-scan.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-scan.repository.d.ts","sourceRoot":"","sources":["../src/content-scan.repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,sBAAsB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;CAC5D"}
@@ -0,0 +1 @@
1
+ export {};
package/dist/engine.d.ts CHANGED
@@ -1,47 +1,47 @@
1
1
  /**
2
2
  * Botanical Engine: Schema Compiler + Query Builder.
3
3
  *
4
- * In v0.4.0 ogni Seed ha una tabella SQL dedicata (`content_{slug}`) con colonne
5
- * reali tipizzate. Questo modulo genera il DDL e costruisce query parametrizzate.
6
- * Non conosce HTTP, auth o UI — è una libreria Node.js pura (C6).
4
+ * In v0.4.0 each Seed has a dedicated SQL table (`content_{slug}`) with
5
+ * real typed columns. This module generates the DDL and builds parameterized queries.
6
+ * It does not know about HTTP, auth, or UI — it's a pure TypeScript library.
7
7
  */
8
8
  import type { Seed, Branch, SelectOptions, ParameterizedQuery } from './types.js';
9
9
  /**
10
- * Genera `CREATE TABLE IF NOT EXISTS content_{slug}` con colonne di sistema
11
- * + una colonna per ogni Branch. Funzione pura: stesso Seed → stesso SQL.
10
+ * Generates `CREATE TABLE IF NOT EXISTS content_{slug}` with system columns
11
+ * + one column per Branch. Pure function: same Seed → same SQL.
12
12
  */
13
13
  export declare function generateCreateTable(seed: Seed): string;
14
14
  /**
15
- * Genera la tabella bozze `content_{slug}_drafts` per i Seed con `allowDrafts: true`.
16
- * Tutte le colonne branch sono nullable (le bozze sono parziali).
17
- * Ritorna null se il Seed non ha `allowDrafts: true`.
15
+ * Generates the drafts table `content_{slug}_drafts` for Seeds with `allowDrafts: true`.
16
+ * All branch columns are nullable (drafts are partial).
17
+ * Returns null if the Seed does not have `allowDrafts: true`.
18
18
  */
19
19
  export declare function generateDraftTable(seed: Seed): string | null;
20
20
  /**
21
- * Genera `ALTER TABLE content_{slug} ADD COLUMN {alias} {type}`.
22
- * Nuove colonne sono sempre nullable (limite SQLite su ALTER TABLE).
21
+ * Generates `ALTER TABLE content_{slug} ADD COLUMN {alias} {type}`.
22
+ * New columns are always nullable (SQLite limit on ALTER TABLE).
23
23
  */
24
24
  export declare function generateAddColumn(seed: Seed, branch: Branch): string;
25
25
  /**
26
- * Genera indici B-tree per status, created_at e ogni Branch filtrabile
27
- * con tipo indicizzabile (text, number, date, boolean).
26
+ * Generates B-tree indexes for status, created_at and every filterable Branch
27
+ * with an indexable type (text, number, date, boolean).
28
28
  */
29
29
  export declare function generateIndexes(seed: Seed): string[];
30
30
  /**
31
- * Genera la virtual table FTS5 per i Branch text/richtext indicizzabili.
32
- * Ritorna null se il Seed non ha branch con search abilitato.
31
+ * Generates the FTS5 virtual table for indexable text/richtext Branches.
32
+ * Returns null if the Seed has no branches with search enabled.
33
33
  */
34
34
  export declare function generateFtsTable(seed: Seed): string | null;
35
35
  /**
36
- * Genera i 3 trigger SQLite (insert/update/delete) che mantengono la FTS
37
- * sincronizzata automaticamenteelimina la necessità di syncFts manuale.
38
- * Ritorna array vuoto se il Seed non ha branch indicizzabili.
36
+ * Generates the 3 SQLite triggers (insert/update/delete) that keep the FTS
37
+ * automatically synchronizedeliminates the need for manual syncFts.
38
+ * Returns an empty array if the Seed has no indexable branches.
39
39
  */
40
40
  export declare function generateFtsTriggers(seed: Seed): string[];
41
41
  /**
42
- * Costruisce una SELECT parametrizzata su `content_{slug}`.
43
- * Non usa mai json_extract — ogni colonna è una colonna reale.
44
- * Colonne sconosciute nei filtri/orderBy vengono ignorate (fail-closed).
42
+ * Builds a parameterized SELECT on `content_{slug}`.
43
+ * Never uses json_extract — every column is a real column.
44
+ * Unknown columns in filters/orderBy are ignored (fail-closed).
45
45
  */
46
46
  export declare function buildSelectQuery(seed: Seed, options?: SelectOptions): ParameterizedQuery;
47
47
  export interface SchemaColumn {
@@ -51,17 +51,17 @@ export interface SchemaColumn {
51
51
  isPk: boolean;
52
52
  }
53
53
  /**
54
- * Ritorna la lista di colonne attese per la tabella di un Seed.
55
- * Usato da `beech seed:load --diff` per confrontare schema attuale vs atteso.
54
+ * Returns the list of expected columns for a Seed's table.
55
+ * Used by `beech seed:load --diff` to compare current vs expected schema.
56
56
  */
57
57
  export declare function getExpectedColumns(seed: Seed): SchemaColumn[];
58
58
  /**
59
- * Serializza un valore per la scrittura nel DB.
59
+ * Serializes a value for writing to the DB.
60
60
  * boolean → 0/1 | date → Unix timestamp | json/asset-list → JSON string
61
61
  */
62
62
  export declare function serializeForDb(branch: Branch, value: unknown): string | number | null;
63
63
  /**
64
- * Deserializza un valore letto dal DB per la risposta API.
64
+ * Deserializes a value read from the DB for the API response.
65
65
  * 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object
66
66
  */
67
67
  export declare function deserializeFromDb(branch: Branch, value: unknown): unknown;
package/dist/engine.js CHANGED
@@ -71,8 +71,8 @@ function normalizeAssetListValue(rawValue) {
71
71
  }
72
72
  // ---- DDL Generators ----
73
73
  /**
74
- * Genera `CREATE TABLE IF NOT EXISTS content_{slug}` con colonne di sistema
75
- * + una colonna per ogni Branch. Funzione pura: stesso Seed → stesso SQL.
74
+ * Generates `CREATE TABLE IF NOT EXISTS content_{slug}` with system columns
75
+ * + one column per Branch. Pure function: same Seed → same SQL.
76
76
  */
77
77
  export function generateCreateTable(seed) {
78
78
  const table = tableName(seed);
@@ -97,9 +97,9 @@ export function generateCreateTable(seed) {
97
97
  return lines.join('\n');
98
98
  }
99
99
  /**
100
- * Genera la tabella bozze `content_{slug}_drafts` per i Seed con `allowDrafts: true`.
101
- * Tutte le colonne branch sono nullable (le bozze sono parziali).
102
- * Ritorna null se il Seed non ha `allowDrafts: true`.
100
+ * Generates the drafts table `content_{slug}_drafts` for Seeds with `allowDrafts: true`.
101
+ * All branch columns are nullable (drafts are partial).
102
+ * Returns null if the Seed does not have `allowDrafts: true`.
103
103
  */
104
104
  export function generateDraftTable(seed) {
105
105
  if (!seed.allowDrafts)
@@ -114,7 +114,7 @@ export function generateDraftTable(seed) {
114
114
  for (const branch of seed.branches) {
115
115
  const { sqlType } = BRANCH_TYPE_SQL[branch.type];
116
116
  let col = ` ${branch.alias} ${sqlType}`;
117
- // boolean CHECK: in SQLite, NULL IN (0,1) → NULL, che passa il CHECK (solo FALSE lo fallisce)
117
+ // boolean CHECK: in SQLite, NULL IN (0,1) → NULL, which passes the CHECK (only FALSE fails it)
118
118
  if (branch.type === 'boolean')
119
119
  col += ` CHECK (${branch.alias} IN (0, 1))`;
120
120
  lines.push(col + ',');
@@ -124,16 +124,16 @@ export function generateDraftTable(seed) {
124
124
  return lines.join('\n');
125
125
  }
126
126
  /**
127
- * Genera `ALTER TABLE content_{slug} ADD COLUMN {alias} {type}`.
128
- * Nuove colonne sono sempre nullable (limite SQLite su ALTER TABLE).
127
+ * Generates `ALTER TABLE content_{slug} ADD COLUMN {alias} {type}`.
128
+ * New columns are always nullable (SQLite limit on ALTER TABLE).
129
129
  */
130
130
  export function generateAddColumn(seed, branch) {
131
131
  const { sqlType } = BRANCH_TYPE_SQL[branch.type];
132
132
  return `ALTER TABLE ${tableName(seed)} ADD COLUMN ${branch.alias} ${sqlType};`;
133
133
  }
134
134
  /**
135
- * Genera indici B-tree per status, created_at e ogni Branch filtrabile
136
- * con tipo indicizzabile (text, number, date, boolean).
135
+ * Generates B-tree indexes for status, created_at and every filterable Branch
136
+ * with an indexable type (text, number, date, boolean).
137
137
  */
138
138
  export function generateIndexes(seed) {
139
139
  const table = tableName(seed);
@@ -152,8 +152,8 @@ export function generateIndexes(seed) {
152
152
  return indexes;
153
153
  }
154
154
  /**
155
- * Genera la virtual table FTS5 per i Branch text/richtext indicizzabili.
156
- * Ritorna null se il Seed non ha branch con search abilitato.
155
+ * Generates the FTS5 virtual table for indexable text/richtext Branches.
156
+ * Returns null if the Seed has no branches with search enabled.
157
157
  */
158
158
  export function generateFtsTable(seed) {
159
159
  const rtBranches = indexableSearchBranches(seed);
@@ -170,9 +170,9 @@ export function generateFtsTable(seed) {
170
170
  ].join('\n');
171
171
  }
172
172
  /**
173
- * Genera i 3 trigger SQLite (insert/update/delete) che mantengono la FTS
174
- * sincronizzata automaticamenteelimina la necessità di syncFts manuale.
175
- * Ritorna array vuoto se il Seed non ha branch indicizzabili.
173
+ * Generates the 3 SQLite triggers (insert/update/delete) that keep the FTS
174
+ * automatically synchronizedeliminates the need for manual syncFts.
175
+ * Returns an empty array if the Seed has no indexable branches.
176
176
  */
177
177
  export function generateFtsTriggers(seed) {
178
178
  const rtBranches = indexableSearchBranches(seed);
@@ -208,9 +208,9 @@ export function generateFtsTriggers(seed) {
208
208
  }
209
209
  // ---- Query Builder ----
210
210
  /**
211
- * Costruisce una SELECT parametrizzata su `content_{slug}`.
212
- * Non usa mai json_extract — ogni colonna è una colonna reale.
213
- * Colonne sconosciute nei filtri/orderBy vengono ignorate (fail-closed).
211
+ * Builds a parameterized SELECT on `content_{slug}`.
212
+ * Never uses json_extract — every column is a real column.
213
+ * Unknown columns in filters/orderBy are ignored (fail-closed).
214
214
  */
215
215
  export function buildSelectQuery(seed, options = {}) {
216
216
  const table = tableName(seed);
@@ -369,8 +369,8 @@ function buildFilterCondition(col, type, cond, bindings) {
369
369
  return null;
370
370
  }
371
371
  /**
372
- * Ritorna la lista di colonne attese per la tabella di un Seed.
373
- * Usato da `beech seed:load --diff` per confrontare schema attuale vs atteso.
372
+ * Returns the list of expected columns for a Seed's table.
373
+ * Used by `beech seed:load --diff` to compare current vs expected schema.
374
374
  */
375
375
  export function getExpectedColumns(seed) {
376
376
  return [
@@ -389,7 +389,7 @@ export function getExpectedColumns(seed) {
389
389
  }
390
390
  // ---- Serialization / Deserialization ----
391
391
  /**
392
- * Serializza un valore per la scrittura nel DB.
392
+ * Serializes a value for writing to the DB.
393
393
  * boolean → 0/1 | date → Unix timestamp | json/asset-list → JSON string
394
394
  */
395
395
  export function serializeForDb(branch, value) {
@@ -427,7 +427,7 @@ export function serializeForDb(branch, value) {
427
427
  }
428
428
  }
429
429
  /**
430
- * Deserializza un valore letto dal DB per la risposta API.
430
+ * Deserializes a value read from the DB for the API response.
431
431
  * 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object
432
432
  */
433
433
  export function deserializeFromDb(branch, value) {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Identifier abstraction. Hides direct calls to {@link crypto.randomUUID}
3
+ * so callers can swap in deterministic generators during tests for stable
4
+ * snapshot assertions and predictable insert ordering.
5
+ */
6
+ export interface IIdGenerator {
7
+ /**
8
+ * Generates a new universally unique identifier.
9
+ * Production implementation delegates to crypto.randomUUID().
10
+ * Test implementations return deterministic values for snapshot assertions.
11
+ */
12
+ uuid(): string;
13
+ }
14
+ /**
15
+ * Production singleton. Delegates to the host runtime CSPRNG-backed UUID
16
+ * generator. Stateless and therefore safe to share across requests.
17
+ */
18
+ export declare const SystemIdGenerator: IIdGenerator;
19
+ //# sourceMappingURL=id-generator.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Production singleton. Delegates to the host runtime CSPRNG-backed UUID
3
+ * generator. Stateless and therefore safe to share across requests.
4
+ */
5
+ export const SystemIdGenerator = {
6
+ uuid: () => crypto.randomUUID(),
7
+ };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @beechcms/core - Botanical Engine
3
3
  *
4
- * Pacchetto condiviso del monorepo Beech CMS.
5
- * In v0.4.0 il Botanical Engine è un compilatore di schema SQL: legge i Seed
6
- * TypeScript e genera DDL deterministico + query parametrizzate.
4
+ * Shared package of the Beech CMS monorepo.
5
+ * In v0.4.0 the Botanical Engine is a SQL schema compiler: it reads TypeScript
6
+ * Seeds and generates deterministic DDL + parameterized queries.
7
7
  *
8
8
  * @module @beechcms/core
9
9
  */
@@ -20,4 +20,21 @@ export * from './idempotency.repository.js';
20
20
  export * from './media.repository.js';
21
21
  export * from './storage.js';
22
22
  export * from './policies.js';
23
+ export * from './auth/hash-provider.js';
24
+ export * from './auth/token-service.js';
25
+ export * from './auth/user.repository.js';
26
+ export * from './auth/session.repository.js';
27
+ export * from './auth/password-reset-token.repository.js';
28
+ export * from './rate-limit/rate-limiter.js';
29
+ export * from './observability/activity-logger.js';
30
+ export * from './observability/activity-log.repository.js';
31
+ export * from './observability/analytics.repository.js';
32
+ export * from './notifications/notification.repository.js';
33
+ export * from './notifications/notification-service.js';
34
+ export * from './widget/widget.repository.js';
35
+ export * from './search/search.repository.js';
36
+ export * from './content-scan.repository.js';
37
+ export * from './clock.js';
38
+ export * from './id-generator.js';
39
+ export * from './seed-registry.js';
23
40
  //# 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"}
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"}
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @beechcms/core - Botanical Engine
3
3
  *
4
- * Pacchetto condiviso del monorepo Beech CMS.
5
- * In v0.4.0 il Botanical Engine è un compilatore di schema SQL: legge i Seed
6
- * TypeScript e genera DDL deterministico + query parametrizzate.
4
+ * Shared package of the Beech CMS monorepo.
5
+ * In v0.4.0 the Botanical Engine is a SQL schema compiler: it reads TypeScript
6
+ * Seeds and generates deterministic DDL + parameterized queries.
7
7
  *
8
8
  * @module @beechcms/core
9
9
  */
@@ -20,3 +20,20 @@ export * from './idempotency.repository.js';
20
20
  export * from './media.repository.js';
21
21
  export * from './storage.js';
22
22
  export * from './policies.js';
23
+ export * from './auth/hash-provider.js';
24
+ export * from './auth/token-service.js';
25
+ export * from './auth/user.repository.js';
26
+ export * from './auth/session.repository.js';
27
+ export * from './auth/password-reset-token.repository.js';
28
+ export * from './rate-limit/rate-limiter.js';
29
+ export * from './observability/activity-logger.js';
30
+ export * from './observability/activity-log.repository.js';
31
+ export * from './observability/analytics.repository.js';
32
+ export * from './notifications/notification.repository.js';
33
+ export * from './notifications/notification-service.js';
34
+ export * from './widget/widget.repository.js';
35
+ export * from './search/search.repository.js';
36
+ export * from './content-scan.repository.js';
37
+ export * from './clock.js';
38
+ export * from './id-generator.js';
39
+ export * from './seed-registry.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Notification service contract.
3
+ *
4
+ * High-level port that callers use to emit a notification without knowing
5
+ * about the underlying repository, the database, or background scheduling.
6
+ * Mirrors the email module pattern: handlers depend on the service, the
7
+ * service depends on the repository, the repository depends on D1.
8
+ *
9
+ * @module @beechcms/core/notifications/notification-service
10
+ */
11
+ import type { NotificationType } from './notification.repository.js';
12
+ export interface CreateNotificationInput {
13
+ title: string;
14
+ message: string;
15
+ type?: NotificationType;
16
+ }
17
+ export interface INotificationService {
18
+ /**
19
+ * Emit a notification.
20
+ *
21
+ * Implementations decide whether to fire-and-forget (production: schedules
22
+ * the underlying repository write through `executionCtx.waitUntil`) or to
23
+ * wait inline (tests). Like the activity logger, this method MUST NOT
24
+ * throw to the caller: a failed notification must never break the public
25
+ * request that triggered it.
26
+ */
27
+ notify(input: CreateNotificationInput): Promise<void> | void;
28
+ }
29
+ //# sourceMappingURL=notification-service.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Notification repository contract.
3
+ *
4
+ * Persists in-app notifications (admin inbox) and exposes the aggregate stats
5
+ * the GET /notifications handler needs to build a strong ETag without reading
6
+ * every row.
7
+ *
8
+ * @module @beechcms/core/notifications/notification.repository
9
+ */
10
+ export type NotificationType = 'info' | 'success' | 'warning' | 'error';
11
+ export interface NotificationRecord {
12
+ id: string;
13
+ title: string;
14
+ message: string;
15
+ type: NotificationType;
16
+ isRead: boolean;
17
+ createdAt: number;
18
+ }
19
+ export interface NotificationStats {
20
+ totalCount: number;
21
+ latestCreatedAt: number;
22
+ readCount: number;
23
+ }
24
+ export interface INotificationRepository {
25
+ /**
26
+ * Return the most recent notifications, newest first.
27
+ *
28
+ * The `limit` parameter is required to prevent unbounded reads — the inbox
29
+ * could grow large over time and clients only ever render a window.
30
+ */
31
+ list(limit: number): Promise<NotificationRecord[]>;
32
+ /**
33
+ * Return aggregate counters used to build the GET /notifications ETag.
34
+ *
35
+ * Computing `totalCount`, `latestCreatedAt` and `readCount` on the database
36
+ * side lets the handler skip serialising the full list when nothing has
37
+ * changed since the client's last poll, saving bandwidth on the dashboard.
38
+ */
39
+ stats(): Promise<NotificationStats>;
40
+ /**
41
+ * Insert a new notification and return its generated id so callers (e.g.
42
+ * the public form submission flow) can correlate the notification with the
43
+ * triggering request.
44
+ */
45
+ create(record: Omit<NotificationRecord, 'id' | 'createdAt' | 'isRead'>): Promise<string>;
46
+ markRead(notificationId: string): Promise<void>;
47
+ markUnread(notificationId: string): Promise<void>;
48
+ markAllRead(): Promise<void>;
49
+ delete(notificationId: string): Promise<void>;
50
+ }
51
+ //# sourceMappingURL=notification.repository.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Notification repository contract.
3
+ *
4
+ * Persists in-app notifications (admin inbox) and exposes the aggregate stats
5
+ * the GET /notifications handler needs to build a strong ETag without reading
6
+ * every row.
7
+ *
8
+ * @module @beechcms/core/notifications/notification.repository
9
+ */
10
+ export {};
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Activity Log read-side repository contract.
3
+ *
4
+ * Used by handlers that surface the audit trail back to the user (settings
5
+ * activity tab, recent activity feed on the dashboard). Kept separate from
6
+ * {@link IActivityLogger} because writers and readers have different
7
+ * lifecycles: writes are fire-and-forget background tasks; reads are blocking
8
+ * request paths that need filters and pagination.
9
+ *
10
+ * @module @beechcms/core/observability/activity-log.repository
11
+ */
12
+ import type { ActivityAction, EntityType } from './activity-logger.js';
13
+ export interface ActivityLogRecord {
14
+ id: string;
15
+ userId: string;
16
+ userEmail: string;
17
+ userName: string | null;
18
+ action: ActivityAction;
19
+ entityType: EntityType;
20
+ entityId: string;
21
+ entitySlug: string | null;
22
+ details: Record<string, unknown> | null;
23
+ createdAt: number;
24
+ }
25
+ export interface ActivityLogListOptions {
26
+ /** Restrict the result to a specific user. Used by the per-user activity tab. */
27
+ userId?: string;
28
+ /** Restrict the result to a specific content seed. Used by per-seed feeds. */
29
+ entitySlug?: string;
30
+ /** Hard upper bound on the number of rows returned. */
31
+ limit: number;
32
+ }
33
+ export interface CountSinceOptions {
34
+ action: ActivityAction;
35
+ entityType: EntityType;
36
+ /** Lower bound (inclusive) for `createdAt` in seconds since epoch. */
37
+ sinceTimestamp: number;
38
+ }
39
+ export interface IActivityLogRepository {
40
+ /**
41
+ * Return the most recent activity entries matching the given filters.
42
+ *
43
+ * The list is always ordered by `createdAt` DESC so callers get newest-first
44
+ * data without paying extra sort costs in TypeScript. Used by the settings
45
+ * activity tab (per-user) and by the stats recent-activity feed (global).
46
+ */
47
+ list(options: ActivityLogListOptions): Promise<ActivityLogRecord[]>;
48
+ /**
49
+ * Count entries with the given `action` and `entityType` whose `createdAt`
50
+ * is greater-than-or-equal to `sinceTimestamp`.
51
+ *
52
+ * Used by the dashboard `/stats/total` widget to surface today/week/month
53
+ * create-event counts without hardcoding SQL in the handler. Each period
54
+ * is one call so the repository contract stays narrow and deterministic.
55
+ */
56
+ countSince(options: CountSinceOptions): Promise<number>;
57
+ }
58
+ //# sourceMappingURL=activity-log.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-log.repository.d.ts","sourceRoot":"","sources":["../../src/observability/activity-log.repository.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtE,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,cAAc,CAAA;IACtB,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,cAAc,CAAA;IACtB,UAAU,EAAE,UAAU,CAAA;IACtB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,IAAI,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAA;IAEnE;;;;;;;OAOG;IACH,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACxD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Activity Logger contract.
3
+ *
4
+ * Records side-effect events ("a user did X to entity Y") for the audit trail
5
+ * displayed in the dashboard's settings activity tab and the recent activity
6
+ * feed. The interface is intentionally framework-agnostic so it can be
7
+ * fulfilled in production by a D1-backed implementation, in tests by an
8
+ * in-memory implementation, and in the future by remote sinks.
9
+ *
10
+ * @module @beechcms/core/observability/activity-logger
11
+ */
12
+ export type ActivityAction = 'create' | 'update' | 'delete' | 'upload';
13
+ export type EntityType = 'content' | 'media';
14
+ /**
15
+ * Identifies the human (or machine) actor that triggered the event.
16
+ *
17
+ * The actor MUST be assembled by the caller (typically a Hono handler that has
18
+ * already authenticated the request). The logger never reaches into a
19
+ * framework-specific context to discover who is acting — this avoids hidden
20
+ * coupling and lets the same logger run inside CLI scripts and background
21
+ * jobs.
22
+ */
23
+ export interface ActivityActor {
24
+ id: string;
25
+ email: string;
26
+ name?: string | null;
27
+ }
28
+ export interface ActivityLogEntry {
29
+ action: ActivityAction;
30
+ entityType: EntityType;
31
+ entityId: string;
32
+ entitySlug?: string;
33
+ details?: Record<string, unknown>;
34
+ actor: ActivityActor;
35
+ }
36
+ export interface IActivityLogger {
37
+ /**
38
+ * Persist a single activity entry.
39
+ *
40
+ * Implementations decide whether to fire-and-forget (Cloudflare
41
+ * `executionCtx.waitUntil`) or to wait inline (test environments).
42
+ * In either case the call MUST NOT throw to the caller: logging is
43
+ * observability, never a hard dependency of the request being served.
44
+ * Internal errors must be swallowed and logged via `console.error` so the
45
+ * mainline response path is never disturbed by audit-trail failures.
46
+ */
47
+ log(entry: ActivityLogEntry): Promise<void> | void;
48
+ }
49
+ //# sourceMappingURL=activity-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-logger.d.ts","sourceRoot":"","sources":["../../src/observability/activity-logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AACtE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,CAAA;AAE5C;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,KAAK,EAAE,aAAa,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;OASG;IACH,GAAG,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CACnD"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Activity Logger contract.
3
+ *
4
+ * Records side-effect events ("a user did X to entity Y") for the audit trail
5
+ * displayed in the dashboard's settings activity tab and the recent activity
6
+ * feed. The interface is intentionally framework-agnostic so it can be
7
+ * fulfilled in production by a D1-backed implementation, in tests by an
8
+ * in-memory implementation, and in the future by remote sinks.
9
+ *
10
+ * @module @beechcms/core/observability/activity-logger
11
+ */
12
+ export {};
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Metric tracked by the analytics table. Today only request counts and
3
+ * unique-visitor counts are recorded; new metric names should be appended
4
+ * to this union and to the implementation's switch statement.
5
+ */
6
+ export type AnalyticsMetric = 'requests' | 'visitors';
7
+ /**
8
+ * Read/write contract for the analytics counters that power the dashboard
9
+ * widgets (per-day request totals, sparklines, system health proxy).
10
+ *
11
+ * Implementations must use idempotent upserts so the recording middleware
12
+ * can be invoked once per request without producing duplicates when the
13
+ * same day/seed/metric tuple is touched repeatedly.
14
+ */
15
+ export interface IAnalyticsRepository {
16
+ /**
17
+ * Upserts a request counter for the given seed at the current day bucket.
18
+ * The day bucket (Unix timestamp truncated to midnight UTC) is computed
19
+ * internally from the implementation's clock so callers never need to
20
+ * pass a timestamp. Implementations must use INSERT ... ON CONFLICT DO
21
+ * UPDATE to remain idempotent under concurrent calls within the same day.
22
+ */
23
+ recordRequest(seedSlug: string): Promise<void>;
24
+ /**
25
+ * Returns the total count for the given metric since sinceTimestamp.
26
+ * Used by the stats handler for total request counts and visitor counts.
27
+ * The aggregation sums the stored counter values, not row counts.
28
+ */
29
+ sumByMetric(metric: AnalyticsMetric, seedSlug: string, sinceTimestamp: number): Promise<number>;
30
+ /**
31
+ * Returns a map of date strings (YYYY-MM-DD) to request counts since
32
+ * sinceTimestamp, suitable for chart rendering without further
33
+ * transformation by the caller.
34
+ */
35
+ groupByMetric(seedSlug: string, sinceTimestamp: number): Promise<Record<string, number>>;
36
+ }
37
+ //# sourceMappingURL=analytics.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.repository.d.ts","sourceRoot":"","sources":["../../src/observability/analytics.repository.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,UAAU,CAAA;AAErD;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE9C;;;;OAIG;IACH,WAAW,CACT,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,CAAA;IAElB;;;;OAIG;IACH,aAAa,CACX,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACnC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import type { Seed } from '../types.js';
2
+ export interface SearchQueryOptions {
3
+ queryText: string;
4
+ schemaSlug: string | null;
5
+ statusFilter: string | null;
6
+ limit: number;
7
+ cursor: string | null;
8
+ }
9
+ export interface SearchResultRow {
10
+ entryId: string;
11
+ schemaSlug: string;
12
+ slug: string | null;
13
+ status: string;
14
+ title: string | null;
15
+ excerpt: string;
16
+ rank: number;
17
+ }
18
+ export interface SearchCountResult {
19
+ total: number;
20
+ }
21
+ /**
22
+ * Read-only contract for the global FTS5 search route.
23
+ *
24
+ * Implementations encapsulate the UNION ALL query that fans out across all
25
+ * `fts_<seed>` virtual tables. The route handler stays free of D1 and only
26
+ * shapes the final response.
27
+ */
28
+ export interface ISearchRepository {
29
+ /**
30
+ * Executes a UNION ALL full-text search across all FTS-enabled seed tables.
31
+ * Returns at most options.limit + 1 rows so the caller can detect hasMore
32
+ * without a separate count query for the cursor case.
33
+ * Implementations must propagate the EMPTY_QUERY error thrown by
34
+ * buildFtsQuery so the route handler can return an empty result set rather
35
+ * than a 500.
36
+ */
37
+ search(options: SearchQueryOptions, seeds: Seed[]): Promise<SearchResultRow[]>;
38
+ /**
39
+ * Runs the count variant of the FTS query to support the `total` field in
40
+ * the search response. Called in parallel with search() by the route handler
41
+ * with the same filter inputs but without limit/cursor.
42
+ */
43
+ count(options: Omit<SearchQueryOptions, 'limit' | 'cursor'>, seeds: Seed[]): Promise<SearchCountResult>;
44
+ }
45
+ //# sourceMappingURL=search.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.repository.d.ts","sourceRoot":"","sources":["../../src/search/search.repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,MAAM,CAAC,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAA;IAE9E;;;;OAIG;IACH,KAAK,CACH,OAAO,EAAE,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,QAAQ,CAAC,EACrD,KAAK,EAAE,IAAI,EAAE,GACZ,OAAO,CAAC,iBAAiB,CAAC,CAAA;CAC9B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import type { Seed } from './types.js';
2
+ export interface ISeedRegistry {
3
+ /**
4
+ * Returns all seeds as a flat array, preserving insertion order.
5
+ * Decouples callers from the internal storage shape so the registry
6
+ * implementation can change without touching every route handler.
7
+ */
8
+ all(): Seed[];
9
+ /**
10
+ * Returns the seed with the given slug, or null if not found.
11
+ * Provides a single lookup point that can be overridden in tests
12
+ * without rebuilding the full registry object.
13
+ */
14
+ get(slug: string): Seed | null;
15
+ /**
16
+ * Returns seeds that are visible in the dashboard sidebar.
17
+ * A seed is visible when dashboard.hidden is not explicitly true.
18
+ * Eliminates the seeds.filter(s => !s.dashboard?.hidden) pattern
19
+ * that would otherwise be duplicated across route handlers.
20
+ */
21
+ visibleInDashboard(): Seed[];
22
+ /**
23
+ * Returns seeds that have allowPublicRead enabled.
24
+ * Eliminates the seeds.filter(s => s.allowPublicRead) pattern.
25
+ */
26
+ publicReadable(): Seed[];
27
+ /**
28
+ * Returns seeds that have the draft workflow enabled.
29
+ * Eliminates the seeds.filter(s => s.allowDrafts) pattern.
30
+ */
31
+ draftEnabled(): Seed[];
32
+ }
33
+ export declare class SeedRegistry implements ISeedRegistry {
34
+ private readonly seedMap;
35
+ private readonly orderedSeeds;
36
+ constructor(seeds: Seed[]);
37
+ all(): Seed[];
38
+ get(slug: string): Seed | null;
39
+ visibleInDashboard(): Seed[];
40
+ publicReadable(): Seed[];
41
+ draftEnabled(): Seed[];
42
+ }
43
+ /**
44
+ * Named subclass of SeedRegistry for use in test suites.
45
+ * Gives tests a semantic name without requiring any dependency on factory.ts.
46
+ */
47
+ export declare class InMemorySeedRegistry extends SeedRegistry {
48
+ }
49
+ //# sourceMappingURL=seed-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-registry.d.ts","sourceRoot":"","sources":["../src/seed-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,IAAI,IAAI,EAAE,CAAA;IAEb;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAE9B;;;;;OAKG;IACH,kBAAkB,IAAI,IAAI,EAAE,CAAA;IAE5B;;;OAGG;IACH,cAAc,IAAI,IAAI,EAAE,CAAA;IAExB;;;OAGG;IACH,YAAY,IAAI,IAAI,EAAE,CAAA;CACvB;AAED,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;gBAEzB,KAAK,EAAE,IAAI,EAAE;IAKzB,GAAG,IAAI,IAAI,EAAE;IAIb,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI9B,kBAAkB,IAAI,IAAI,EAAE;IAI5B,cAAc,IAAI,IAAI,EAAE;IAIxB,YAAY,IAAI,IAAI,EAAE;CAGvB;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;CAAG"}
@@ -0,0 +1,29 @@
1
+ export class SeedRegistry {
2
+ seedMap;
3
+ orderedSeeds;
4
+ constructor(seeds) {
5
+ this.orderedSeeds = seeds;
6
+ this.seedMap = new Map(seeds.map(seed => [seed.slug, seed]));
7
+ }
8
+ all() {
9
+ return this.orderedSeeds;
10
+ }
11
+ get(slug) {
12
+ return this.seedMap.get(slug) ?? null;
13
+ }
14
+ visibleInDashboard() {
15
+ return this.orderedSeeds.filter(seed => seed.dashboard?.hidden !== true);
16
+ }
17
+ publicReadable() {
18
+ return this.orderedSeeds.filter(seed => seed.allowPublicRead === true);
19
+ }
20
+ draftEnabled() {
21
+ return this.orderedSeeds.filter(seed => seed.allowDrafts === true);
22
+ }
23
+ }
24
+ /**
25
+ * Named subclass of SeedRegistry for use in test suites.
26
+ * Gives tests a semantic name without requiring any dependency on factory.ts.
27
+ */
28
+ export class InMemorySeedRegistry extends SeedRegistry {
29
+ }
@@ -21,18 +21,18 @@ export interface ValidateSeedPayloadResult {
21
21
  hasAnyValidField: boolean;
22
22
  }
23
23
  /**
24
- * Foundation comune per validazione e sanitizzazione payload schema-driven.
25
- * Usata da Public API e riusabile nel Botanical Engine.
24
+ * Common foundation for schema-driven validation and sanitization of payloads.
25
+ * Used by the Public API and reusable in the Botanical Engine.
26
26
  *
27
- * Ordine di esecuzione obbligatorio al momento della scrittura:
27
+ * Mandatory execution order at the time of writing:
28
28
  * validate raw → hash (privacy policy) → store
29
- * L'hashing avviene DOPO la validazione, non prima.
30
- * Questo garantisce che il valore validato sia il valore raw originale,
31
- * non il digest — evitando false failure su campi con formato (es. email).
29
+ * Hashing occurs AFTER validation, not before.
30
+ * This ensures that the validated value is the original raw value,
31
+ * not the digest — avoiding false failures on fields with formats (e.g., email).
32
32
  */
33
33
  export declare function validateAndSanitizeSeedPayload(seed: Seed, payload: Record<string, unknown>, options?: ValidateSeedPayloadOptions): ValidateSeedPayloadResult;
34
34
  /**
35
- * Valida status content supportati dal CMS.
35
+ * Validates content status supported by the CMS.
36
36
  */
37
37
  export declare function isValidContentStatus(value: unknown): value is 'draft' | 'review' | 'published';
38
38
  /**
@@ -452,14 +452,14 @@ function validateBranchValue(branch, alias, rawValue, options) {
452
452
  }
453
453
  }
454
454
  /**
455
- * Foundation comune per validazione e sanitizzazione payload schema-driven.
456
- * Usata da Public API e riusabile nel Botanical Engine.
455
+ * Common foundation for schema-driven validation and sanitization of payloads.
456
+ * Used by the Public API and reusable in the Botanical Engine.
457
457
  *
458
- * Ordine di esecuzione obbligatorio al momento della scrittura:
458
+ * Mandatory execution order at the time of writing:
459
459
  * 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).
460
+ * Hashing occurs AFTER validation, not before.
461
+ * This ensures that the validated value is the original raw value,
462
+ * not the digest — avoiding false failures on fields with formats (e.g., email).
463
463
  */
464
464
  export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
465
465
  const normalizedOptions = {
@@ -564,7 +564,7 @@ export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
564
564
  };
565
565
  }
566
566
  /**
567
- * Valida status content supportati dal CMS.
567
+ * Validates content status supported by the CMS.
568
568
  */
569
569
  export function isValidContentStatus(value) {
570
570
  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":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,OAAO,CAAA;CAAE,GACf;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACpD;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,cAAc,EAAE,KAAK,GAAG,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACvC,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAErF;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAExF;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAA;IAEjF;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAEvE;;;;OAIG;IACH,UAAU,CACR,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,eAAe,EAAE,CAAC,CAAA;CAC9B"}
@@ -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.1",
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",