@beechcms/api 0.6.0-preview.4 → 0.6.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 (121) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/assets/chart-FUiy4I7R.js +39 -0
  4. package/assets/dashboard/assets/index-BlehzIfd.js +644 -0
  5. package/assets/dashboard/assets/index-Dve7eEqA.css +1 -0
  6. package/assets/dashboard/assets/{pie-chart-recharts-BMWpjFzf.js → pie-chart-recharts-iaYz4FeR.js} +1 -1
  7. package/assets/dashboard/assets/timeseries-chart-recharts-C8Lcylom.js +1 -0
  8. package/assets/dashboard/beechLogoDark.svg +48 -48
  9. package/assets/dashboard/index.html +17 -18
  10. package/assets/dashboard/noResult.svg +42 -42
  11. package/assets/dashboard/sol.svg +3 -3
  12. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  13. package/dist/auth/constants.d.ts +5 -5
  14. package/dist/features/automations/{automations.handler.d.ts → api/automations.handler.d.ts} +1 -1
  15. package/dist/features/automations/{automation-runner.utils.d.ts → engine/automation-runner.utils.d.ts} +1 -1
  16. package/dist/features/automations/{action-executors → executors}/edit-field.executor.d.ts +1 -1
  17. package/dist/features/automations/{action-executors → executors}/index.d.ts +1 -1
  18. package/dist/features/automations/{action-executors → executors}/send-mail.executor.d.ts +1 -1
  19. package/dist/features/automations/{action-executors → executors}/set-variable.executor.d.ts +1 -1
  20. package/dist/features/automations/{action-executors → executors}/webhook.executor.d.ts +1 -1
  21. package/dist/features/automations/{when-evaluator.d.ts → filters/when-evaluator.d.ts} +1 -1
  22. package/dist/features/automations/index.d.ts +7 -7
  23. package/dist/features/content/constants.d.ts +10 -10
  24. package/dist/features/content/handlers/kanban-move.d.ts +3 -0
  25. package/dist/features/content/handlers/kanban-position.d.ts +3 -0
  26. package/dist/features/content/handlers/view-config.d.ts +4 -0
  27. package/dist/{search.d.ts → features/search/search.d.ts} +1 -1
  28. package/dist/features/upload/index.d.ts +9 -0
  29. package/dist/{widget.d.ts → features/widget/widget.d.ts} +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +1057 -418
  32. package/dist/{middleware.d.ts → middleware/auth.middleware.d.ts} +1 -1
  33. package/dist/middleware/repository.middleware.d.ts +2 -1
  34. package/dist/public/access-policy.d.ts +1 -1
  35. package/dist/public/problem-details.d.ts +1 -0
  36. package/dist/public/public-errors.d.ts +4 -4
  37. package/dist/rate-limit/cloudflare-rate-limiter.d.ts +4 -1
  38. package/dist/rate-limit/in-memory-rate-limiter.d.ts +2 -1
  39. package/dist/shared/{schema-mutator.d1.d.ts → db/migrations/schema-mutator.d1.d.ts} +1 -0
  40. package/dist/shared/db/repositories/content.repository.d1.d.ts +259 -0
  41. package/dist/shared/db/repositories/kanban-position.repository.d1.d.ts +9 -0
  42. package/dist/shared/{seed-layout.repository.d1.d.ts → db/repositories/seed-layout.repository.d1.d.ts} +3 -1
  43. package/dist/shared/email/email.provider.d.ts +1 -0
  44. package/dist/{features → shared}/email/email.types.d.ts +1 -16
  45. package/dist/{features → shared}/email/providers/resend.d.ts +1 -2
  46. package/dist/{features → shared}/email/providers/smtp.d.ts +1 -2
  47. package/dist/shared/jobs/queue-consumer.d.ts +9 -0
  48. package/dist/shared/services/queue/cloudflare-queue-service.d.ts +11 -0
  49. package/dist/shared/services/queue/in-memory-queue-service.d.ts +16 -0
  50. package/dist/shared/storage/s3-bucket.d.ts +0 -1
  51. package/dist/shared/storage/upload.d.ts +7 -0
  52. package/dist/{media-utils.d.ts → shared/utils/media-utils.d.ts} +4 -2
  53. package/dist/types.d.ts +93 -2
  54. package/migrations/0000_v040_base.sql +265 -265
  55. package/migrations/0029_automations.sql +14 -14
  56. package/migrations/0030_test_seeds.sql +125 -125
  57. package/package.json +5 -4
  58. package/assets/dashboard/assets/chart-BwlCt-IA.js +0 -39
  59. package/assets/dashboard/assets/index-BcVN-DNy.js +0 -642
  60. package/assets/dashboard/assets/index-Cf1yRCqG.css +0 -1
  61. package/assets/dashboard/assets/timeseries-chart-recharts-8_GwAFR0.js +0 -1
  62. package/dist/features/email/email.provider.d.ts +0 -37
  63. package/dist/shared/content.repository.d1.d.ts +0 -68
  64. package/dist/upload.d.ts +0 -16
  65. package/dist/auth/{bcrypt-hash-provider.d.ts → providers/hash.provider.d.ts} +0 -0
  66. package/dist/auth/{jose-token-service.d.ts → providers/jwt-token.service.d.ts} +0 -0
  67. package/dist/auth/{login.d.ts → utils/login-helpers.d.ts} +0 -0
  68. package/dist/auth/{refresh.d.ts → utils/refresh-token.d.ts} +0 -0
  69. package/dist/features/automations/{automations.schema.d.ts → api/automations.schema.d.ts} +16 -16
  70. /package/dist/features/automations/{automation-runner.d.ts → engine/automation-runner.d.ts} +0 -0
  71. /package/dist/features/automations/{cron-runner.d.ts → engine/cron-runner.d.ts} +0 -0
  72. /package/dist/features/automations/{cron-runner.utils.d.ts → engine/cron-runner.utils.d.ts} +0 -0
  73. /package/dist/features/automations/{context-resolver.d.ts → evaluator/context-resolver.d.ts} +0 -0
  74. /package/dist/features/automations/{template-grammar.d.ts → evaluator/template-grammar.d.ts} +0 -0
  75. /package/dist/features/automations/{var-access-resolver.d.ts → evaluator/var-access-resolver.d.ts} +0 -0
  76. /package/dist/features/automations/{action-executors → executors}/create-entry.executor.d.ts +0 -0
  77. /package/dist/features/automations/{filter-translation.d.ts → filters/filter-translation.d.ts} +0 -0
  78. /package/dist/features/automations/{when-pushdown.d.ts → filters/when-pushdown.d.ts} +0 -0
  79. /package/dist/{search-utils.d.ts → features/search/search-utils.d.ts} +0 -0
  80. /package/dist/shared/{demo-data-sql.d.ts → db/migrations/demo-data-sql.d.ts} +0 -0
  81. /package/dist/shared/{automations.repository.d1.d.ts → db/repositories/automations.repository.d1.d.ts} +0 -0
  82. /package/dist/shared/{base.repository.d1.d.ts → db/repositories/base.repository.d1.d.ts} +0 -0
  83. /package/dist/shared/{d1-activity-log.repository.d.ts → db/repositories/d1-activity-log.repository.d.ts} +0 -0
  84. /package/dist/shared/{d1-analytics.repository.d.ts → db/repositories/d1-analytics.repository.d.ts} +0 -0
  85. /package/dist/shared/{d1-content-scan.repository.d.ts → db/repositories/d1-content-scan.repository.d.ts} +0 -0
  86. /package/dist/shared/{d1-notification.repository.d.ts → db/repositories/d1-notification.repository.d.ts} +0 -0
  87. /package/dist/shared/{d1-password-reset-token.repository.d.ts → db/repositories/d1-password-reset-token.repository.d.ts} +0 -0
  88. /package/dist/shared/{d1-search.repository.d.ts → db/repositories/d1-search.repository.d.ts} +0 -0
  89. /package/dist/shared/{d1-session.repository.d.ts → db/repositories/d1-session.repository.d.ts} +0 -0
  90. /package/dist/shared/{d1-setup-checklist.repository.d.ts → db/repositories/d1-setup-checklist.repository.d.ts} +0 -0
  91. /package/dist/shared/{d1-user.repository.d.ts → db/repositories/d1-user.repository.d.ts} +0 -0
  92. /package/dist/shared/{d1-widget.repository.d.ts → db/repositories/d1-widget.repository.d.ts} +0 -0
  93. /package/dist/shared/{dashboard-layout.repository.d1.d.ts → db/repositories/dashboard-layout.repository.d1.d.ts} +0 -0
  94. /package/dist/shared/{demo-data.repository.d1.d.ts → db/repositories/demo-data.repository.d1.d.ts} +0 -0
  95. /package/dist/shared/{idempotency.repository.d1.d.ts → db/repositories/idempotency.repository.d1.d.ts} +0 -0
  96. /package/dist/shared/{in-memory-seed.repository.d.ts → db/repositories/in-memory-seed.repository.d.ts} +0 -0
  97. /package/dist/shared/{media.repository.d1.d.ts → db/repositories/media.repository.d1.d.ts} +0 -0
  98. /package/dist/shared/{seed.repository.d1.d.ts → db/repositories/seed.repository.d1.d.ts} +0 -0
  99. /package/dist/shared/{site-settings.repository.d1.d.ts → db/repositories/site-settings.repository.d1.d.ts} +0 -0
  100. /package/dist/shared/{system-stats.repository.d1.d.ts → db/repositories/system-stats.repository.d1.d.ts} +0 -0
  101. /package/dist/{features → shared}/email/email.service.d.ts +0 -0
  102. /package/dist/{features → shared}/email/index.d.ts +0 -0
  103. /package/dist/{features → shared}/email/templates/automation-mail.d.ts +0 -0
  104. /package/dist/{features → shared}/email/templates/password-changed.d.ts +0 -0
  105. /package/dist/{features → shared}/email/templates/password-reset.d.ts +0 -0
  106. /package/dist/{features → shared}/email/templates/shell.d.ts +0 -0
  107. /package/dist/shared/{fts-sync.d.ts → jobs/fts-sync.d.ts} +0 -0
  108. /package/dist/shared/{apply-policies.d.ts → policies/apply-policies.d.ts} +0 -0
  109. /package/dist/shared/{d1-activity-logger.d.ts → services/activity-log/d1-activity-logger.d.ts} +0 -0
  110. /package/dist/shared/{in-memory-activity-logger.d.ts → services/activity-log/in-memory-activity-logger.d.ts} +0 -0
  111. /package/dist/shared/{seed-registry-cache.d.ts → services/cache/seed-registry-cache.d.ts} +0 -0
  112. /package/dist/shared/{fixed-clock.d.ts → services/clock/fixed-clock.d.ts} +0 -0
  113. /package/dist/shared/{sequential-id-generator.d.ts → services/id-generator/sequential-id-generator.d.ts} +0 -0
  114. /package/dist/shared/{background-notification-service.d.ts → services/notification/background-notification-service.d.ts} +0 -0
  115. /package/dist/shared/{in-memory-notification-service.d.ts → services/notification/in-memory-notification-service.d.ts} +0 -0
  116. /package/dist/shared/{qstash-notification-service.d.ts → services/notification/qstash-notification-service.d.ts} +0 -0
  117. /package/dist/shared/{execution-context-scheduler.d.ts → services/scheduler/execution-context-scheduler.d.ts} +0 -0
  118. /package/dist/shared/{content-utils.d.ts → utils/content-utils.d.ts} +0 -0
  119. /package/dist/shared/{query-utils.d.ts → utils/query-utils.d.ts} +0 -0
  120. /package/dist/shared/{request-utils.d.ts → utils/request-utils.d.ts} +0 -0
  121. /package/dist/shared/{storage-utils.d.ts → utils/storage-utils.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { Context, Next } from 'hono';
2
- import type { Env, Variables } from './types';
2
+ import type { Env, Variables } from '../types';
3
3
  /**
4
4
  * JWT authentication middleware. Reads the Bearer token from the Authorization
5
5
  * header and delegates verification to the ITokenService injected in context.
@@ -1,4 +1,4 @@
1
- import type { ContentRepository, IdempotencyRepository, MediaRepository, SystemStatsRepository, IUserRepository, ISessionRepository, IPasswordResetTokenRepository, IActivityLogRepository, INotificationRepository, IWidgetRepository, ISearchRepository, IAnalyticsRepository, IContentScanRepository, IClock, IIdGenerator, IAutomationRunner, IAutomationRepository, IScheduler, ISiteSettingsRepository, IDemoDataRepository, ISeedLayoutRepository, ISeedRepository, ISchemaMutator, IDashboardLayoutRepository, BeechHooks } from '@beechcms/core';
1
+ import type { ContentRepository, IdempotencyRepository, MediaRepository, SystemStatsRepository, IUserRepository, ISessionRepository, IPasswordResetTokenRepository, IActivityLogRepository, INotificationRepository, IWidgetRepository, ISearchRepository, IAnalyticsRepository, IContentScanRepository, IClock, IIdGenerator, IAutomationRunner, IAutomationRepository, IScheduler, ISiteSettingsRepository, IDemoDataRepository, ISeedLayoutRepository, ISeedRepository, ISchemaMutator, IDashboardLayoutRepository, BeechHooks, IKanbanPositionRepository } from '@beechcms/core';
2
2
  import type { Env, Variables } from '../types';
3
3
  interface RepositoryOverrides {
4
4
  repository?: ContentRepository;
@@ -25,6 +25,7 @@ interface RepositoryOverrides {
25
25
  seedRepository?: ISeedRepository;
26
26
  schemaMutator?: ISchemaMutator;
27
27
  dashboardLayoutRepository?: IDashboardLayoutRepository;
28
+ kanbanPositionRepository?: IKanbanPositionRepository;
28
29
  hooks?: BeechHooks;
29
30
  }
30
31
  export declare const repositoryMiddleware: (overrides?: RepositoryOverrides) => import("hono").MiddlewareHandler<{
@@ -6,7 +6,7 @@ export declare function checkPublicOperation(seed: Seed, operation: PublicOperat
6
6
  } | {
7
7
  readonly ok: false;
8
8
  readonly error: {
9
- readonly error: "Forbidden";
9
+ readonly error: 'Forbidden';
10
10
  readonly message: `Public ${string} is not allowed for content type '${string}'.`;
11
11
  };
12
12
  };
@@ -20,6 +20,7 @@ type PublicProblemInput = {
20
20
  status: 400 | 401 | 403 | 404 | 405 | 409 | 422 | 429 | 500 | 501;
21
21
  detail: string;
22
22
  errors?: PublicProblemDetailItem[];
23
+ headers?: Record<string, string>;
23
24
  };
24
25
  export declare function internalErrorDetail(env: {
25
26
  ENV?: string;
@@ -3,11 +3,11 @@
3
3
  */
4
4
  export declare const PUBLIC_ERRORS: {
5
5
  readonly API_KEY_UNAUTHORIZED: {
6
- readonly error: "Unauthorized";
7
- readonly message: "Missing or invalid API key. Provide a valid key via X-API-Key header.";
6
+ readonly error: 'Unauthorized';
7
+ readonly message: 'Missing or invalid API key. Provide a valid key via X-API-Key header.';
8
8
  };
9
9
  readonly API_KEY_FORBIDDEN: {
10
- readonly error: "Forbidden";
11
- readonly message: "Public API access is not configured for this instance.";
10
+ readonly error: 'Forbidden';
11
+ readonly message: 'Public API access is not configured for this instance.';
12
12
  };
13
13
  };
@@ -1,6 +1,9 @@
1
1
  import type { IRateLimiter, RateLimitResult } from '@beechcms/core';
2
2
  export declare class CloudflareRateLimiter implements IRateLimiter {
3
3
  private readonly binding;
4
- constructor(binding: RateLimit);
4
+ private readonly options;
5
+ constructor(binding: RateLimit, options?: {
6
+ failClosed?: boolean;
7
+ });
5
8
  checkLimit(key: string): Promise<RateLimitResult>;
6
9
  }
@@ -1,7 +1,8 @@
1
1
  import type { IRateLimiter, RateLimitResult } from '@beechcms/core';
2
2
  export declare class InMemoryRateLimiter implements IRateLimiter {
3
3
  private readonly maxAllowedHits;
4
+ private readonly windowSeconds;
4
5
  private readonly hitCounts;
5
- constructor(maxAllowedHits: number);
6
+ constructor(maxAllowedHits: number, windowSeconds?: number);
6
7
  checkLimit(key: string): Promise<RateLimitResult>;
7
8
  }
@@ -9,4 +9,5 @@ export declare class D1SchemaMutator implements ISchemaMutator {
9
9
  dropColumn(table: string, column: string): Promise<void>;
10
10
  renameColumn(table: string, from: string, to: string): Promise<void>;
11
11
  execDestructive(statements: string[]): Promise<void>;
12
+ fetchRows(table: string, columns: string[]): Promise<Record<string, unknown>[]>;
12
13
  }
@@ -0,0 +1,259 @@
1
+ import { ContentRepository, type BulkFieldUpdate, type BatchWrite, type DraftSummary, type Seed, type SelectOptions, type RepositoryOptions, type BeechHooks } from '@beechcms/core';
2
+ import { BaseD1Repository } from './base.repository.d1';
3
+ /**
4
+ * D1 (Cloudflare SQLite) implementation of {@link ContentRepository}.
5
+ *
6
+ * Persists each seed's entries in a dedicated `content_{slug}` table, with multi-relation
7
+ * branches stored in separate junction tables (`rel_{slug}_{alias}`) rather than as columns,
8
+ * since D1/SQLite has no native array column type. Draft data mirrors this shape in
9
+ * `content_{slug}_drafts` and `rel_{slug}_{alias}_drafts` tables.
10
+ *
11
+ * Lifecycle hooks ({@link BeechHooks}) fire around create/update/delete: `before*` hooks run
12
+ * inside the try block and may transform the payload or abort by throwing; `after*` hooks run
13
+ * once the write has already committed and therefore cannot roll back the mutation.
14
+ */
15
+ export declare class D1ContentRepository extends BaseD1Repository implements ContentRepository {
16
+ private readonly hooks?;
17
+ constructor(database: D1Database, hooks?: BeechHooks | undefined);
18
+ /** Assembles the {@link HookContext} passed to every lifecycle hook invocation. */
19
+ private hookCtx;
20
+ /**
21
+ * Deserializes a DB row using the Seed's branch definitions.
22
+ * Skips multi-relation branches — their values come from junction tables.
23
+ */
24
+ private rowToData;
25
+ /**
26
+ * Fetches multi-relation arrays for a single entry and attaches them to `data`.
27
+ *
28
+ * @param seed The seed schema definition.
29
+ * @param entryId Id of the entry to fetch relation targets for.
30
+ * @param data Mutated in place: one array property per multi-relation branch alias.
31
+ */
32
+ private attachMultiRelations;
33
+ /**
34
+ * Builds one junction-table statement per multi-relation branch, selecting the rows whose
35
+ * `parent_id` falls within the page defined by `idQuery` (the page selection re-projected to
36
+ * ids only). Using a subquery instead of a bound `IN (?, ...)` id list keeps the statements
37
+ * eligible for the same batch as the page query and avoids D1's bound-parameter limit.
38
+ *
39
+ * The subquery is wrapped in `SELECT id FROM (...)` because `kanbanOrder` projections carry an
40
+ * extra `kp.position` column, and `IN` requires a single-column subselect.
41
+ */
42
+ private multiRelStatements;
43
+ /**
44
+ * Groups junction rows by parent and attaches them to the entries: one array property per
45
+ * multi-relation branch alias, per entry. `relResults[i]` must correspond to `branches[i]`.
46
+ *
47
+ * @param entries Mutated in place.
48
+ */
49
+ private attachMultiRelationRows;
50
+ /**
51
+ * Finds multiple entries for a given seed table based on the provided query options.
52
+ * Executes a single batch (one D1 round-trip) containing:
53
+ * 1. A selection query to retrieve the items for the current page (with columns, limits, offsets, and ordering).
54
+ * 2. A count query (using `isCount: true`) to retrieve the total number of matched items matching the same filters.
55
+ * 3. One junction-table query per multi-relation branch, scoped to the page via an id subquery.
56
+ *
57
+ * @param seed The seed schema definition.
58
+ * @param options Filtering, search, ordering, and pagination options.
59
+ * @returns An object containing the list of serialized entries and the total count of matched items.
60
+ * @throws RepositoryError if the database batch execution fails.
61
+ */
62
+ findMany(seed: Seed, options: SelectOptions): Promise<{
63
+ items: Record<string, any>[];
64
+ total: number;
65
+ }>;
66
+ /**
67
+ * Fetches a single entry by id, including its multi-relation arrays.
68
+ * @throws EntryNotFoundError if no row with `id` exists in the seed's table.
69
+ * @throws RepositoryError on any other database failure.
70
+ */
71
+ findById(seed: Seed, id: string): Promise<Record<string, any>>;
72
+ /**
73
+ * Fetches a single entry by its unique `slug`, including its multi-relation arrays.
74
+ * @throws EntryNotFoundError if no row with `slug` exists in the seed's table.
75
+ * @throws RepositoryError on any other database failure.
76
+ */
77
+ findBySlug(seed: Seed, slug: string): Promise<Record<string, any>>;
78
+ /**
79
+ * Computes aggregate facets for a seed: entry counts per `status`, and the distinct set
80
+ * of tag values present across all entries for each `tags`-typed branch (via `json_each`
81
+ * over the JSON-serialized tag column).
82
+ */
83
+ getFacets(seed: Seed): Promise<{
84
+ statuses: Record<string, number>;
85
+ tagsByColumn: Record<string, string[]>;
86
+ }>;
87
+ /**
88
+ * Checks whether `slug` is already taken in the seed's table.
89
+ * @param excludeId When provided (e.g. during an update), excludes that entry's own row from the check.
90
+ */
91
+ existsSlug(seed: Seed, slug: string, excludeId?: string): Promise<boolean>;
92
+ /**
93
+ * Builds the `INSERT` statement for an entry's main table row. Multi-relation branch
94
+ * values are skipped (they're written as separate junction-table inserts by the caller).
95
+ */
96
+ private buildCreateMainStmt;
97
+ /**
98
+ * Builds the `UPDATE` statement for an entry's main table row from whichever branch
99
+ * aliases are present in `data` (partial update — absent keys are left untouched).
100
+ * Multi-relation branches present in `data` are returned as `junctionUpdates` instead
101
+ * of being included in the SQL, since they require separate delete+insert statements.
102
+ *
103
+ * @returns `stmt: null` when there is nothing to update on the main row (no scalar
104
+ * fields and no `status` change) even if junction updates are still needed.
105
+ */
106
+ private buildUpdateMainStmt;
107
+ /** Builds one `INSERT` per target id into a live multi-relation junction table, preserving array order via `position`. */
108
+ private buildJunctionInserts;
109
+ /** Builds one `INSERT` per target id into a draft multi-relation junction table, preserving array order via `position`. */
110
+ private buildDraftJunctionInserts;
111
+ /**
112
+ * Validates that every relation target referenced by a draft still exists before publish,
113
+ * for both single-relation columns and multi-relation draft junction rows.
114
+ *
115
+ * @throws RelationTargetNotFoundError if any referenced target id is missing from its target seed's table.
116
+ * @returns The multi-relation branches and their resolved (validated) draft target ids,
117
+ * so the caller can reuse them without re-querying the draft junction tables.
118
+ */
119
+ private validatePublishDraftRelations;
120
+ /**
121
+ * Builds the junction-table statements for one array-typed {@link BulkFieldUpdate}:
122
+ * `array_replace` (delete-all + reinsert), `array_add` (append via `INSERT OR IGNORE`
123
+ * with position computed from current max), or `array_remove` (targeted delete).
124
+ * Returns an empty array for a no-op (e.g. `array_remove` with an empty value list).
125
+ */
126
+ private getBulkArrayUpdateStmts;
127
+ /**
128
+ * Applies a bulk field update to a single entry as one batch: a main-table `UPDATE` for
129
+ * `set`-kind fields plus any junction statements for array-kind fields.
130
+ * @throws Error('not-found') if the main `UPDATE` affects zero rows (id doesn't exist).
131
+ */
132
+ private processBulkUpdateSingle;
133
+ /**
134
+ * Sequentially applies a bulk field update to each id in `chunk`, isolating per-id
135
+ * failures (not-found, FK violation, or other error) into the `failed` list rather than
136
+ * aborting the whole chunk.
137
+ */
138
+ private processBulkUpdateChunk;
139
+ /**
140
+ * Creates a new entry: runs `beforeCreate` (which may transform the payload), inserts the
141
+ * main row plus any multi-relation junction rows as a single batch, then runs `afterCreate`.
142
+ *
143
+ * @throws SlugConflictError if `slug` already exists for this seed.
144
+ * @throws RepositoryError on any other database failure.
145
+ * @remarks `afterCreate` runs after the batch has committed, so it cannot roll back the write.
146
+ */
147
+ create(seed: Seed, id: string, slug: string, status: string, data: Record<string, any>, options?: RepositoryOptions): Promise<void>;
148
+ /**
149
+ * Partially updates an existing entry: runs `beforeUpdate`, applies main-row and
150
+ * junction-table changes as a batch, then runs `afterUpdate`. A no-op payload (no scalar
151
+ * fields, no multi-relation fields, no status change) returns without touching the database.
152
+ *
153
+ * @throws EntryNotFoundError if the main-row `UPDATE` affects zero rows.
154
+ * @throws RepositoryError on any other database failure.
155
+ * @remarks `afterUpdate` runs after the batch has committed, so it cannot roll back the write.
156
+ */
157
+ update(seed: Seed, id: string, data: Record<string, any>, status?: string, options?: RepositoryOptions): Promise<void>;
158
+ /**
159
+ * Deletes an entry: runs `beforeDelete`, reads the row (for the return value), deletes it,
160
+ * then runs `afterDelete`. Relies on database-level `ON DELETE CASCADE` for junction rows.
161
+ *
162
+ * @returns The deserialized row as it existed immediately before deletion.
163
+ * @throws EntryNotFoundError if no row with `id` exists.
164
+ * @remarks `afterDelete` runs after the delete has committed, so it cannot roll back the write.
165
+ */
166
+ delete(seed: Seed, id: string, options?: RepositoryOptions): Promise<{
167
+ row: Record<string, any>;
168
+ }>;
169
+ /**
170
+ * Atomically increments/decrements a numeric field in a single conditional `UPDATE`
171
+ * (guarded by optional `min`/`max` bounds evaluated against the new value), avoiding
172
+ * read-modify-write races.
173
+ *
174
+ * @throws RepositoryError if `fieldName` isn't a `number`-typed branch on `seed`, or if
175
+ * the conditional `UPDATE` affects zero rows (entry not found, or bound exceeded).
176
+ */
177
+ mutateField(seed: Seed, id: string, fieldName: string, operation: {
178
+ type: 'increment' | 'decrement';
179
+ value: number;
180
+ }, options?: {
181
+ min?: number;
182
+ max?: number;
183
+ }): Promise<{
184
+ newValue: number;
185
+ }>;
186
+ /**
187
+ * Executes a heterogeneous list of {@link BatchWrite} operations (create/update/mutateField)
188
+ * as a single atomic `database.batch` call. Does not invoke lifecycle hooks — callers needing
189
+ * hook side effects should use {@link create}/{@link update}/{@link mutateField} instead.
190
+ *
191
+ * @throws RepositoryError if any operation references a non-numeric field for `mutateField`,
192
+ * or on any other database failure.
193
+ */
194
+ runBatch(operations: BatchWrite[]): Promise<void>;
195
+ /**
196
+ * Upserts a draft row for `entryId` (`INSERT ... ON CONFLICT DO UPDATE` keyed on `entry_id`)
197
+ * plus a full delete+reinsert of each multi-relation branch's draft junction rows.
198
+ * @throws RepositoryError if `seed.allowDrafts` is false, or on any database failure.
199
+ */
200
+ saveDraft(seed: Seed, entryId: string, data: Record<string, any>): Promise<void>;
201
+ /**
202
+ * Fetches the draft row for `entryId`, if any, merged with its multi-relation draft arrays.
203
+ * @returns `null` if `seed.allowDrafts` is false or no draft row exists.
204
+ */
205
+ getDraft(seed: Seed, entryId: string): Promise<Record<string, any> | null>;
206
+ /** Checks whether a draft row exists for `entryId`. Always `false` if `seed.allowDrafts` is false. */
207
+ hasDraft(seed: Seed, entryId: string): Promise<boolean>;
208
+ /**
209
+ * Promotes a draft to the live entry: validates all relation targets referenced by the
210
+ * draft still exist, copies the draft's scalar fields into the live row, replaces the
211
+ * live junction rows with the draft's, and deletes the draft — all as one batch.
212
+ * A no-op if `seed.allowDrafts` is false.
213
+ *
214
+ * @throws EntryNotFoundError if no draft row exists for `entryId`.
215
+ * @throws RelationTargetNotFoundError if any relation the draft references no longer exists.
216
+ */
217
+ publishDraft(seed: Seed, entryId: string): Promise<void>;
218
+ /**
219
+ * Applies the same set of field updates across many entries, chunked at 50 ids per batch
220
+ * to bound per-call payload size. Per-id failures are collected rather than aborting the
221
+ * whole operation — see {@link processBulkUpdateSingle} for failure classification.
222
+ */
223
+ bulkUpdate(seed: Seed, ids: string[], fields: Record<string, BulkFieldUpdate>): Promise<{
224
+ updated: number;
225
+ failed: Array<{
226
+ id: string;
227
+ reason: string;
228
+ }>;
229
+ }>;
230
+ /** Deletes the draft row for `entryId`, if any. A no-op if `seed.allowDrafts` is false. */
231
+ deleteDraft(seed: Seed, entryId: string): Promise<void>;
232
+ /**
233
+ * Finds every pending draft across the given seeds, `UNION ALL`-ing one query per seed
234
+ * so results span heterogeneous `content_{slug}_drafts` tables, then left-joins each
235
+ * against `activity_logs` to attribute the most recent "draft saved" action.
236
+ *
237
+ * @param seeds Seeds to search; drafts are looked up per-seed since draft tables aren't unified.
238
+ * @returns Draft summaries ordered by most-recently-updated first. `title` falls back to the
239
+ * draft's own `id` if the display-name column is blank on both the draft and its live counterpart.
240
+ */
241
+ findPendingDrafts(seeds: Seed[]): Promise<DraftSummary[]>;
242
+ /**
243
+ * Updates an entry's field values (optional) and its kanban board position atomically in one
244
+ * batch: an optional main-row/junction update via {@link buildUpdateMainStmt}, plus an upsert
245
+ * into `kanban_positions` keyed on `(seed_slug, entry_id, axis_branch_id)`.
246
+ *
247
+ * @param patch Field changes to apply alongside the drag/drop, or `null` if only position changed.
248
+ * @param position Opaque fractional-index (or similar) position value used for ordering within the axis bucket.
249
+ * @param axisBranchId Identifies which branch/column axis this position is relative to (e.g. status column).
250
+ * @remarks Unlike {@link create}/{@link update}, this method does not invoke `beforeUpdate`/`afterUpdate`
251
+ * lifecycle hooks — the `_ctx.actor` parameter is currently unused. Kanban drag-and-drop writes
252
+ * therefore bypass hook-based side effects (e.g. audit logging) that `update()` would trigger.
253
+ */
254
+ updateWithKanbanPosition(seed: Seed, id: string, patch: Record<string, unknown> | null, position: string, axisBranchId: string, _ctx: {
255
+ actor: string;
256
+ }): Promise<{
257
+ success: boolean;
258
+ }>;
259
+ }
@@ -0,0 +1,9 @@
1
+ import type { IKanbanPositionRepository, KanbanPositionRecord } from '@beechcms/core';
2
+ export declare class D1KanbanPositionRepository implements IKanbanPositionRepository {
3
+ private readonly db;
4
+ constructor(db: D1Database);
5
+ getColumn(seedSlug: string, axisBranchId: string): Promise<Map<string, string>>;
6
+ setPosition(seedSlug: string, entryId: string, axisBranchId: string, position: string): Promise<void>;
7
+ remove(seedSlug: string, entryId: string, axisBranchId: string): Promise<void>;
8
+ rebalance(seedSlug: string, axisBranchId: string, ordered: KanbanPositionRecord[]): Promise<void>;
9
+ }
@@ -1,4 +1,4 @@
1
- import type { ISeedLayoutRepository, SeedLayoutRecord, FormLayout } from '@beechcms/core';
1
+ import { type ISeedLayoutRepository, type SeedLayoutRecord, type FormLayout, type SeedViewConfig } from '@beechcms/core';
2
2
  export declare class D1SeedLayoutRepository implements ISeedLayoutRepository {
3
3
  private readonly db;
4
4
  constructor(db: D1Database);
@@ -6,4 +6,6 @@ export declare class D1SeedLayoutRepository implements ISeedLayoutRepository {
6
6
  getAllAsMap(): Promise<Map<string, FormLayout>>;
7
7
  upsert(slug: string, layout: FormLayout, updatedBy: string): Promise<void>;
8
8
  remove(slug: string): Promise<void>;
9
+ getViewConfig(slug: string): Promise<SeedViewConfig | null>;
10
+ setViewConfig(slug: string, config: SeedViewConfig, updatedBy: string): Promise<void>;
9
11
  }
@@ -0,0 +1 @@
1
+ export type { EmailProvider, OutboundEmail } from '@beechcms/core';
@@ -12,7 +12,7 @@
12
12
  * 2. Add the corresponding translation in the `COPY` object of every
13
13
  * file in `templates/`. TypeScript will flag missing keys.
14
14
  */
15
- export declare const SUPPORTED_EMAIL_LOCALES: readonly ["en", "it"];
15
+ export declare const SUPPORTED_EMAIL_LOCALES: readonly ['en', 'it'];
16
16
  export type EmailLocale = (typeof SUPPORTED_EMAIL_LOCALES)[number];
17
17
  /**
18
18
  * Resolves an unverified locale string (e.g., from a request body)
@@ -23,20 +23,6 @@ export type EmailLocale = (typeof SUPPORTED_EMAIL_LOCALES)[number];
23
23
  * @returns A valid `EmailLocale`, always.
24
24
  */
25
25
  export declare function resolveEmailLocale(raw: unknown): EmailLocale;
26
- /**
27
- * The resolved email message that the provider receives and sends.
28
- * It is constructed by the service by combining the call parameters
29
- * with the template builder output.
30
- */
31
- export interface OutboundEmail {
32
- /** Sender address in RFC 5321 format (e.g., "Beech CMS <noreply@beechcms.dev>"). */
33
- from: string;
34
- /** List of recipient addresses. Must contain at least one element. */
35
- to: string[];
36
- subject: string;
37
- /** Complete HTML body. Must be a valid HTML document (see `templates/shell.ts`). */
38
- html: string;
39
- }
40
26
  /**
41
27
  * Shared parameters for every email sending function in `email.service.ts`.
42
28
  * Specific functions extend this type with the additional fields
@@ -87,7 +73,6 @@ export interface AutomationMailParams {
87
73
  /** Plain text or HTML — passed verbatim to provider. */
88
74
  body: string;
89
75
  apiKey?: string;
90
- resendApiKey?: string;
91
76
  from?: string;
92
77
  provider?: 'smtp' | 'resend';
93
78
  smtpBaseUrl?: string;
@@ -1,5 +1,4 @@
1
- import type { EmailProvider } from '../email.provider';
2
- import type { OutboundEmail } from '../email.types';
1
+ import type { EmailProvider, OutboundEmail } from '@beechcms/core';
3
2
  /**
4
3
  * Resend implementation of EmailProvider.
5
4
  *
@@ -1,5 +1,4 @@
1
- import type { EmailProvider } from '../email.provider';
2
- import type { OutboundEmail } from '../email.types';
1
+ import type { EmailProvider, OutboundEmail } from '@beechcms/core';
3
2
  export interface SmtpProviderConfig {
4
3
  /** Base URL of the Mailpit HTTP API, e.g. http://localhost:8025 */
5
4
  baseUrl: string;
@@ -0,0 +1,9 @@
1
+ import type { JobRegistry, QueueMessage } from '@beechcms/core';
2
+ import type { Env } from '../../types';
3
+ /**
4
+ * Consumer-side dispatch. Mirrors index.ts `scheduled()`: builds an
5
+ * engine-mediated JobContext from bindings (NEVER exposes env.DB to jobs) and
6
+ * routes each message to its handler. ack() on success/unknown-name (drop),
7
+ * retry() on throw so Cloudflare re-delivers.
8
+ */
9
+ export declare function dispatchQueueBatch(batch: MessageBatch<QueueMessage>, env: Env, _ctx: ExecutionContext, jobs: JobRegistry): Promise<void>;
@@ -0,0 +1,11 @@
1
+ import type { IQueueService, QueueMessage } from '@beechcms/core';
2
+ /**
3
+ * Production producer. Sends a QueueMessage envelope onto the Cloudflare Queue
4
+ * binding. Cloudflare guarantees at-least-once delivery + retries on the
5
+ * consumer side, so enqueue only needs to hand off the message.
6
+ */
7
+ export declare class CloudflareQueueService implements IQueueService {
8
+ private readonly queue;
9
+ constructor(queue: Queue<QueueMessage>);
10
+ enqueue<T>(name: string, payload: T): Promise<void>;
11
+ }
@@ -0,0 +1,16 @@
1
+ import type { IQueueService, JobRegistry, JobContext } from '@beechcms/core';
2
+ type ScheduleBackgroundTask = (task: Promise<unknown>) => void;
3
+ /**
4
+ * Dev/offline + test fallback used when the QUEUE binding is absent. Runs the
5
+ * matching handler in-process. In production this is never selected. Uses
6
+ * executionCtx.waitUntil when available so enqueue stays non-blocking; falls
7
+ * back to awaiting inline (tests) otherwise.
8
+ */
9
+ export declare class InMemoryQueueService implements IQueueService {
10
+ private readonly jobs;
11
+ private readonly context;
12
+ private readonly scheduleBackgroundTask?;
13
+ constructor(jobs: JobRegistry, context: JobContext, scheduleBackgroundTask?: ScheduleBackgroundTask | undefined);
14
+ enqueue<T>(name: string, payload: T): Promise<void>;
15
+ }
16
+ export {};
@@ -17,7 +17,6 @@ export declare class S3Bucket implements BeechBucket {
17
17
  cdnUrl?: string;
18
18
  });
19
19
  put(key: string, body: ArrayBuffer | Uint8Array | ReadableStream, options?: PutBucketOptions): Promise<void>;
20
- private streamToUint8Array;
21
20
  get(key: string): Promise<GetBucketResult | null>;
22
21
  delete(key: string): Promise<void>;
23
22
  head(key: string): Promise<{
@@ -0,0 +1,7 @@
1
+ export declare function deleteR2Objects(c: {
2
+ var: {
3
+ bucket: any;
4
+ mediaRepository: any;
5
+ systemStatsRepository: any;
6
+ };
7
+ }, objectKeys: string[]): Promise<void>;
@@ -10,9 +10,10 @@ import type { Seed } from '@beechcms/core';
10
10
  * Es: "https://x.com/api/media/1739123456-avatar.png" → "1739123456-avatar.png"
11
11
  *
12
12
  * @param mediaUrl - URL completo o path (es. /api/media/123-foto.png)
13
+ * @param cdnUrl - URL CDN opzionale configurato
13
14
  * @returns Chiave R2 o null se l'URL non è valido
14
15
  */
15
- export declare function extractMediaKey(mediaUrl: string): string | null;
16
+ export declare function extractMediaKey(mediaUrl: string, cdnUrl?: string): string | null;
16
17
  /**
17
18
  * Estrae tutte le chiavi R2 dal data di un'entry.
18
19
  * Cerca nei campi `file` (stringa URL) e `json` (array/oggetto con URL).
@@ -20,6 +21,7 @@ export declare function extractMediaKey(mediaUrl: string): string | null;
20
21
  *
21
22
  * @param seed - Schema del tipo di contenuto
22
23
  * @param entryData - Payload in formato DB (chiavi = branch ID)
24
+ * @param cdnUrl - URL CDN opzionale configurato
23
25
  * @returns Array di chiavi R2 uniche da eliminare
24
26
  */
25
- export declare function extractMediaKeysFromData(seed: Seed, entryData: Record<string, unknown>): string[];
27
+ export declare function extractMediaKeysFromData(seed: Seed, entryData: Record<string, unknown>, cdnUrl?: string): string[];