@beechcms/api 0.6.0-preview.1 → 0.6.0-preview.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  4. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  5. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  6. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  7. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  8. package/assets/dashboard/beechLogoDark.svg +48 -48
  9. package/assets/dashboard/index.html +18 -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/migrations/0000_v040_base.sql +265 -265
  14. package/migrations/0029_automations.sql +14 -14
  15. package/migrations/0030_test_seeds.sql +125 -125
  16. package/package.json +2 -2
  17. package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
  18. package/src/auth/__fixtures__/static-token-service.ts +22 -22
  19. package/src/auth/bcrypt-hash-provider.ts +24 -24
  20. package/src/auth/constants.ts +14 -14
  21. package/src/auth/generate-refresh-token.test.ts +23 -23
  22. package/src/auth/hash-provider.test.ts +55 -55
  23. package/src/auth/jose-token-service.ts +62 -62
  24. package/src/auth/jwt-claims-passthrough.test.ts +87 -87
  25. package/src/auth/login.test.ts +100 -100
  26. package/src/auth/login.ts +78 -78
  27. package/src/auth/refresh.ts +11 -11
  28. package/src/auth/token-service.test.ts +90 -90
  29. package/src/factory.ts +370 -368
  30. package/src/features/automations/__tests__/action-executors.test.ts +255 -255
  31. package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
  32. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
  33. package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
  34. package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
  35. package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
  36. package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
  37. package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
  38. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
  39. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
  40. package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
  41. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
  42. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
  43. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
  44. package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
  45. package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
  46. package/src/features/automations/action-executors/index.ts +37 -37
  47. package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
  48. package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
  49. package/src/features/automations/action-executors/webhook.executor.ts +67 -67
  50. package/src/features/automations/automation-runner.ts +85 -85
  51. package/src/features/automations/automation-runner.utils.ts +47 -47
  52. package/src/features/automations/automations.handler.ts +197 -197
  53. package/src/features/automations/automations.schema.ts +177 -177
  54. package/src/features/automations/context-resolver.ts +152 -152
  55. package/src/features/automations/cron-runner.ts +140 -140
  56. package/src/features/automations/cron-runner.utils.ts +44 -44
  57. package/src/features/automations/filter-translation.ts +46 -46
  58. package/src/features/automations/index.ts +16 -16
  59. package/src/features/automations/template-grammar.ts +245 -245
  60. package/src/features/automations/var-access-resolver.ts +140 -140
  61. package/src/features/automations/when-evaluator.ts +87 -87
  62. package/src/features/automations/when-pushdown.ts +57 -57
  63. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
  64. package/src/features/backrefs/backrefs.handler.ts +168 -168
  65. package/src/features/backrefs/d1-backref.repository.ts +84 -84
  66. package/src/features/backrefs/index.ts +5 -5
  67. package/src/features/content/constants.ts +16 -16
  68. package/src/features/content/handlers/bulk.handler.ts +185 -185
  69. package/src/features/content/handlers/create.ts +129 -129
  70. package/src/features/content/handlers/delete.ts +62 -62
  71. package/src/features/content/handlers/facets.ts +49 -49
  72. package/src/features/content/handlers/get.ts +120 -120
  73. package/src/features/content/handlers/helpers.test.ts +180 -180
  74. package/src/features/content/handlers/helpers.ts +93 -93
  75. package/src/features/content/handlers/list.ts +174 -174
  76. package/src/features/content/handlers/update.ts +174 -174
  77. package/src/features/content/index.ts +26 -26
  78. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  79. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  80. package/src/features/dashboard-layout/index.ts +5 -0
  81. package/src/features/draft/draft.handler.ts +195 -195
  82. package/src/features/draft/draft.middleware.ts +66 -66
  83. package/src/features/draft/index.ts +5 -5
  84. package/src/features/email/email.provider.ts +42 -42
  85. package/src/features/email/email.service.ts +98 -98
  86. package/src/features/email/email.types.ts +119 -119
  87. package/src/features/email/index.ts +33 -33
  88. package/src/features/email/providers/resend.ts +67 -67
  89. package/src/features/email/providers/smtp.ts +55 -55
  90. package/src/features/email/templates/automation-mail.ts +19 -19
  91. package/src/features/email/templates/password-changed.ts +63 -63
  92. package/src/features/email/templates/password-reset.ts +68 -68
  93. package/src/features/email/templates/shell.ts +96 -96
  94. package/src/features/notifications/index.ts +5 -5
  95. package/src/features/notifications/notifications.handler.ts +105 -105
  96. package/src/features/password-reset/index.ts +19 -19
  97. package/src/features/password-reset/request.ts +92 -92
  98. package/src/features/password-reset/reset.ts +102 -102
  99. package/src/features/rotate-field/index.ts +5 -5
  100. package/src/features/rotate-field/rotate-field.handler.ts +132 -132
  101. package/src/features/rotate-field/rotate-field.schema.ts +17 -17
  102. package/src/features/schema/schema.handler.ts +131 -131
  103. package/src/features/seeds/index.ts +5 -5
  104. package/src/features/seeds/seeds.handler.test.ts +1199 -632
  105. package/src/features/seeds/seeds.handler.ts +558 -693
  106. package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
  107. package/src/features/settings/settings.handler.ts +392 -392
  108. package/src/features/setup/index.ts +288 -204
  109. package/src/features/stats/index.ts +5 -5
  110. package/src/features/stats/stats.handler.ts +420 -420
  111. package/src/features/webhooks/index.ts +63 -63
  112. package/src/index.ts +65 -65
  113. package/src/media-utils.ts +82 -82
  114. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  115. package/src/middleware/auth-providers.middleware.ts +45 -45
  116. package/src/middleware/observability.middleware.test.ts +93 -0
  117. package/src/middleware/observability.middleware.ts +70 -70
  118. package/src/middleware/rate-limit.middleware.ts +45 -45
  119. package/src/middleware/repository.middleware.ts +117 -114
  120. package/src/middleware/seed-registry.middleware.test.ts +97 -97
  121. package/src/middleware/seed-registry.middleware.ts +21 -21
  122. package/src/middleware/storage.middleware.ts +25 -25
  123. package/src/middleware.ts +45 -45
  124. package/src/public/access-policy.ts +27 -27
  125. package/src/public/api-key-middleware.ts +57 -57
  126. package/src/public/cache-utils.ts +38 -38
  127. package/src/public/entry-projection.ts +46 -46
  128. package/src/public/idempotency.ts +23 -23
  129. package/src/public/index.ts +16 -16
  130. package/src/public/problem-details.ts +124 -124
  131. package/src/public/public-add.ts +114 -114
  132. package/src/public/public-edit.ts +163 -163
  133. package/src/public/public-errors.ts +19 -19
  134. package/src/public/public-read.ts +63 -63
  135. package/src/public/public-routes.ts +88 -88
  136. package/src/public/query-builder.test.ts +224 -224
  137. package/src/public/query-builder.ts +156 -156
  138. package/src/public/rate-limit-middleware.ts +34 -34
  139. package/src/public/read-list.ts +54 -54
  140. package/src/public/read-single.ts +48 -48
  141. package/src/public/response-builder.ts +30 -30
  142. package/src/public/sanitize.ts +69 -69
  143. package/src/public/slug-utils.ts +18 -18
  144. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
  145. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
  146. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
  147. package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
  148. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
  149. package/src/rate-limit/no-op-rate-limiter.ts +11 -11
  150. package/src/search-utils.test.ts +211 -211
  151. package/src/search-utils.ts +213 -213
  152. package/src/search.ts +65 -65
  153. package/src/shared/apply-policies.test.ts +81 -81
  154. package/src/shared/apply-policies.ts +67 -67
  155. package/src/shared/automations.repository.d1.ts +150 -150
  156. package/src/shared/background-notification-service.test.ts +62 -62
  157. package/src/shared/background-notification-service.ts +52 -52
  158. package/src/shared/base.repository.d1.ts +32 -32
  159. package/src/shared/content-utils.test.ts +165 -165
  160. package/src/shared/content-utils.ts +86 -86
  161. package/src/shared/content.repository.d1.test.ts +507 -458
  162. package/src/shared/content.repository.d1.ts +851 -851
  163. package/src/shared/d1-activity-log.repository.test.ts +140 -140
  164. package/src/shared/d1-activity-log.repository.ts +105 -105
  165. package/src/shared/d1-activity-logger.test.ts +86 -86
  166. package/src/shared/d1-activity-logger.ts +67 -67
  167. package/src/shared/d1-analytics.repository.test.ts +78 -78
  168. package/src/shared/d1-analytics.repository.ts +85 -85
  169. package/src/shared/d1-content-scan.repository.test.ts +80 -80
  170. package/src/shared/d1-content-scan.repository.ts +37 -37
  171. package/src/shared/d1-notification.repository.test.ts +128 -128
  172. package/src/shared/d1-notification.repository.ts +118 -118
  173. package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
  174. package/src/shared/d1-password-reset-token.repository.ts +56 -56
  175. package/src/shared/d1-search.repository.test.ts +87 -87
  176. package/src/shared/d1-search.repository.ts +88 -88
  177. package/src/shared/d1-session.repository.test.ts +125 -125
  178. package/src/shared/d1-session.repository.ts +102 -102
  179. package/src/shared/d1-setup-checklist.repository.ts +36 -36
  180. package/src/shared/d1-user.repository.test.ts +151 -151
  181. package/src/shared/d1-user.repository.ts +119 -119
  182. package/src/shared/d1-widget.repository.test.ts +267 -221
  183. package/src/shared/d1-widget.repository.ts +385 -341
  184. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  185. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  186. package/src/shared/demo-data-sql.ts +216 -54
  187. package/src/shared/demo-data.repository.d1.ts +20 -20
  188. package/src/shared/execution-context-scheduler.ts +13 -13
  189. package/src/shared/fixed-clock.ts +25 -25
  190. package/src/shared/fts-sync.ts +8 -8
  191. package/src/shared/idempotency.repository.d1.test.ts +83 -83
  192. package/src/shared/idempotency.repository.d1.ts +60 -60
  193. package/src/shared/in-memory-activity-logger.ts +19 -19
  194. package/src/shared/in-memory-notification-service.ts +19 -19
  195. package/src/shared/in-memory-seed.repository.ts +58 -51
  196. package/src/shared/media.repository.d1.test.ts +107 -107
  197. package/src/shared/media.repository.d1.ts +68 -68
  198. package/src/shared/qstash-notification-service.test.ts +67 -67
  199. package/src/shared/qstash-notification-service.ts +55 -55
  200. package/src/shared/query-utils.ts +141 -141
  201. package/src/shared/request-utils.ts +26 -26
  202. package/src/shared/schema-mutator.d1.test.ts +142 -0
  203. package/src/shared/schema-mutator.d1.ts +64 -64
  204. package/src/shared/seed-layout.repository.d1.test.ts +114 -114
  205. package/src/shared/seed-layout.repository.d1.ts +62 -62
  206. package/src/shared/seed-registry-cache.test.ts +68 -68
  207. package/src/shared/seed-registry-cache.ts +49 -49
  208. package/src/shared/seed.repository.d1.test.ts +143 -143
  209. package/src/shared/seed.repository.d1.ts +98 -98
  210. package/src/shared/sequential-id-generator.ts +33 -33
  211. package/src/shared/site-settings.repository.d1.ts +53 -53
  212. package/src/shared/storage/factory.ts +41 -41
  213. package/src/shared/storage/s3-bucket.ts +195 -195
  214. package/src/shared/storage-utils.ts +40 -40
  215. package/src/shared/system-stats.repository.d1.test.ts +58 -58
  216. package/src/shared/system-stats.repository.d1.ts +48 -48
  217. package/src/types.ts +90 -87
  218. package/src/upload.ts +198 -198
  219. package/src/widget.test.ts +85 -0
  220. package/src/widget.ts +251 -212
  221. package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
  222. package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
@@ -1,13 +1,13 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { IScheduler } from '@beechcms/core'
6
-
7
- export class ExecutionContextScheduler implements IScheduler {
8
- constructor(private readonly ctx: ExecutionContext) {}
9
-
10
- waitUntil(promise: Promise<unknown>): void {
11
- this.ctx.waitUntil(promise)
12
- }
13
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import type { IScheduler } from '@beechcms/core'
6
+
7
+ export class ExecutionContextScheduler implements IScheduler {
8
+ constructor(private readonly ctx: ExecutionContext) {}
9
+
10
+ waitUntil(promise: Promise<unknown>): void {
11
+ this.ctx.waitUntil(promise)
12
+ }
13
+ }
@@ -1,25 +1,25 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { IClock } from '@beechcms/core'
6
-
7
- const MILLISECONDS_PER_SECOND = 1000
8
-
9
- /**
10
- * Test-only IClock implementation that returns a frozen timestamp on every
11
- * call. Lets specs assert exact values for `createdAt`, `iat`, and
12
- * day-bucket computations without resorting to vi.useFakeTimers or
13
- * patching the global Date constructor.
14
- */
15
- export class FixedClock implements IClock {
16
- constructor(private readonly fixedNowMs: number) {}
17
-
18
- now(): number {
19
- return this.fixedNowMs
20
- }
21
-
22
- nowSeconds(): number {
23
- return Math.floor(this.fixedNowMs / MILLISECONDS_PER_SECOND)
24
- }
25
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import type { IClock } from '@beechcms/core'
6
+
7
+ const MILLISECONDS_PER_SECOND = 1000
8
+
9
+ /**
10
+ * Test-only IClock implementation that returns a frozen timestamp on every
11
+ * call. Lets specs assert exact values for `createdAt`, `iat`, and
12
+ * day-bucket computations without resorting to vi.useFakeTimers or
13
+ * patching the global Date constructor.
14
+ */
15
+ export class FixedClock implements IClock {
16
+ constructor(private readonly fixedNowMs: number) {}
17
+
18
+ now(): number {
19
+ return this.fixedNowMs
20
+ }
21
+
22
+ nowSeconds(): number {
23
+ return Math.floor(this.fixedNowMs / MILLISECONDS_PER_SECOND)
24
+ }
25
+ }
@@ -1,8 +1,8 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- // v0.4.0: FTS sync handled automatically by SQL triggers (generateFtsTriggers).
6
- // syncFts/deleteFts eliminated. This file kept as empty module to avoid import errors
7
- // during the Phase 4 → Phase 6 transition.
8
- export {}
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ // v0.4.0: FTS sync handled automatically by SQL triggers (generateFtsTriggers).
6
+ // syncFts/deleteFts eliminated. This file kept as empty module to avoid import errors
7
+ // during the Phase 4 → Phase 6 transition.
8
+ export {}
@@ -1,83 +1,83 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { describe, it, expect, vi } from 'vitest'
6
- import { D1IdempotencyRepository } from './idempotency.repository.d1'
7
-
8
- function makeMockDb(opts: { firstResult?: unknown } = {}) {
9
- const { firstResult = null } = opts
10
- const runMock = vi.fn().mockResolvedValue({ success: true })
11
- const firstMock = vi.fn().mockResolvedValue(firstResult)
12
- const bindMock = vi.fn(() => ({ run: runMock, first: firstMock }))
13
- const prepareMock = vi.fn(() => ({ bind: bindMock }))
14
- return { db: { prepare: prepareMock } as any, prepareMock, bindMock, runMock }
15
- }
16
-
17
- describe('D1IdempotencyRepository', () => {
18
- describe('lookup', () => {
19
- it('returns a mapped IdempotencyRecord when the key is found', async () => {
20
- const row = {
21
- idempotency_key: 'idem-1',
22
- request_fingerprint: 'fp-abc',
23
- response_status: 200,
24
- response_body: '{"ok":true}',
25
- expires_at: 9999,
26
- }
27
- const { db } = makeMockDb({ firstResult: row })
28
- const result = await new D1IdempotencyRepository(db).lookup('idem-1')
29
- expect(result).toEqual({
30
- key: 'idem-1',
31
- fingerprint: 'fp-abc',
32
- responseStatus: 200,
33
- responseBody: '{"ok":true}',
34
- expiresAt: 9999,
35
- })
36
- })
37
-
38
- it('returns null when the key is not found', async () => {
39
- const { db } = makeMockDb({ firstResult: null })
40
- expect(await new D1IdempotencyRepository(db).lookup('missing')).toBeNull()
41
- })
42
-
43
- it('queries the correct table', async () => {
44
- const { db, prepareMock } = makeMockDb()
45
- await new D1IdempotencyRepository(db).lookup('k')
46
- expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('public_idempotency_keys'))
47
- })
48
- })
49
-
50
- describe('store', () => {
51
- it('calls INSERT INTO public_idempotency_keys with upsert on conflict', async () => {
52
- const { db, prepareMock } = makeMockDb()
53
- await new D1IdempotencyRepository(db).store({
54
- key: 'idem-1', fingerprint: 'fp', responseStatus: 201, responseBody: '{}', expiresAt: 9999,
55
- })
56
- const sql = prepareMock.mock.calls[0][0] as string
57
- expect(sql).toContain('INSERT INTO public_idempotency_keys')
58
- expect(sql).toContain('ON CONFLICT')
59
- })
60
-
61
- it('binds key, fingerprint, status, body, and expiresAt', async () => {
62
- const { db, bindMock } = makeMockDb()
63
- await new D1IdempotencyRepository(db).store({
64
- key: 'idem-1', fingerprint: 'fp-x', responseStatus: 200, responseBody: '{"ok":1}', expiresAt: 8888,
65
- })
66
- const args = bindMock.mock.calls[0] as unknown[]
67
- expect(args).toContain('idem-1')
68
- expect(args).toContain('fp-x')
69
- expect(args).toContain(200)
70
- expect(args).toContain('{"ok":1}')
71
- expect(args).toContain(8888)
72
- })
73
- })
74
-
75
- describe('cleanup', () => {
76
- it('calls DELETE with the expiration timestamp', async () => {
77
- const { db, prepareMock, bindMock } = makeMockDb()
78
- await new D1IdempotencyRepository(db).cleanup(12345)
79
- expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('DELETE FROM public_idempotency_keys'))
80
- expect(bindMock).toHaveBeenCalledWith(12345)
81
- })
82
- })
83
- })
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { describe, it, expect, vi } from 'vitest'
6
+ import { D1IdempotencyRepository } from './idempotency.repository.d1'
7
+
8
+ function makeMockDb(opts: { firstResult?: unknown } = {}) {
9
+ const { firstResult = null } = opts
10
+ const runMock = vi.fn().mockResolvedValue({ success: true })
11
+ const firstMock = vi.fn().mockResolvedValue(firstResult)
12
+ const bindMock = vi.fn(() => ({ run: runMock, first: firstMock }))
13
+ const prepareMock = vi.fn(() => ({ bind: bindMock }))
14
+ return { db: { prepare: prepareMock } as any, prepareMock, bindMock, runMock }
15
+ }
16
+
17
+ describe('D1IdempotencyRepository', () => {
18
+ describe('lookup', () => {
19
+ it('returns a mapped IdempotencyRecord when the key is found', async () => {
20
+ const row = {
21
+ idempotency_key: 'idem-1',
22
+ request_fingerprint: 'fp-abc',
23
+ response_status: 200,
24
+ response_body: '{"ok":true}',
25
+ expires_at: 9999,
26
+ }
27
+ const { db } = makeMockDb({ firstResult: row })
28
+ const result = await new D1IdempotencyRepository(db).lookup('idem-1')
29
+ expect(result).toEqual({
30
+ key: 'idem-1',
31
+ fingerprint: 'fp-abc',
32
+ responseStatus: 200,
33
+ responseBody: '{"ok":true}',
34
+ expiresAt: 9999,
35
+ })
36
+ })
37
+
38
+ it('returns null when the key is not found', async () => {
39
+ const { db } = makeMockDb({ firstResult: null })
40
+ expect(await new D1IdempotencyRepository(db).lookup('missing')).toBeNull()
41
+ })
42
+
43
+ it('queries the correct table', async () => {
44
+ const { db, prepareMock } = makeMockDb()
45
+ await new D1IdempotencyRepository(db).lookup('k')
46
+ expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('public_idempotency_keys'))
47
+ })
48
+ })
49
+
50
+ describe('store', () => {
51
+ it('calls INSERT INTO public_idempotency_keys with upsert on conflict', async () => {
52
+ const { db, prepareMock } = makeMockDb()
53
+ await new D1IdempotencyRepository(db).store({
54
+ key: 'idem-1', fingerprint: 'fp', responseStatus: 201, responseBody: '{}', expiresAt: 9999,
55
+ })
56
+ const sql = prepareMock.mock.calls[0][0] as string
57
+ expect(sql).toContain('INSERT INTO public_idempotency_keys')
58
+ expect(sql).toContain('ON CONFLICT')
59
+ })
60
+
61
+ it('binds key, fingerprint, status, body, and expiresAt', async () => {
62
+ const { db, bindMock } = makeMockDb()
63
+ await new D1IdempotencyRepository(db).store({
64
+ key: 'idem-1', fingerprint: 'fp-x', responseStatus: 200, responseBody: '{"ok":1}', expiresAt: 8888,
65
+ })
66
+ const args = bindMock.mock.calls[0] as unknown[]
67
+ expect(args).toContain('idem-1')
68
+ expect(args).toContain('fp-x')
69
+ expect(args).toContain(200)
70
+ expect(args).toContain('{"ok":1}')
71
+ expect(args).toContain(8888)
72
+ })
73
+ })
74
+
75
+ describe('cleanup', () => {
76
+ it('calls DELETE with the expiration timestamp', async () => {
77
+ const { db, prepareMock, bindMock } = makeMockDb()
78
+ await new D1IdempotencyRepository(db).cleanup(12345)
79
+ expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('DELETE FROM public_idempotency_keys'))
80
+ expect(bindMock).toHaveBeenCalledWith(12345)
81
+ })
82
+ })
83
+ })
@@ -1,60 +1,60 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { IdempotencyRepository, IdempotencyRecord } from '@beechcms/core'
6
- import { BaseD1Repository } from './base.repository.d1.js'
7
-
8
- export class D1IdempotencyRepository extends BaseD1Repository implements IdempotencyRepository {
9
- /**
10
- * Looks up an idempotency record by its key.
11
- */
12
- async lookup(idempotencyKey: string): Promise<IdempotencyRecord | null> {
13
- const idempotencyRecordResult = await this.database.prepare(
14
- `SELECT idempotency_key, request_fingerprint, response_status, response_body, expires_at
15
- FROM public_idempotency_keys WHERE idempotency_key = ? LIMIT 1`
16
- ).bind(idempotencyKey).first<any>()
17
-
18
- if (!idempotencyRecordResult) {
19
- return null
20
- }
21
-
22
- return {
23
- key: idempotencyRecordResult.idempotency_key,
24
- fingerprint: idempotencyRecordResult.request_fingerprint,
25
- responseStatus: idempotencyRecordResult.response_status,
26
- responseBody: idempotencyRecordResult.response_body,
27
- expiresAt: idempotencyRecordResult.expires_at
28
- }
29
- }
30
-
31
- /**
32
- * Stores or updates an idempotency record.
33
- */
34
- async store(idempotencyData: IdempotencyRecord): Promise<void> {
35
- await this.database.prepare(
36
- `INSERT INTO public_idempotency_keys (idempotency_key, request_fingerprint, response_status, response_body, created_at, expires_at)
37
- VALUES (?, ?, ?, ?, ?, ?)
38
- ON CONFLICT(idempotency_key) DO UPDATE SET
39
- request_fingerprint = excluded.request_fingerprint,
40
- response_status = excluded.response_status,
41
- response_body = excluded.response_body,
42
- created_at = excluded.created_at,
43
- expires_at = excluded.expires_at`
44
- ).bind(
45
- idempotencyData.key,
46
- idempotencyData.fingerprint,
47
- idempotencyData.responseStatus,
48
- idempotencyData.responseBody,
49
- Math.floor(Date.now() / 1000),
50
- idempotencyData.expiresAt
51
- ).run()
52
- }
53
-
54
- /**
55
- * Cleans up expired idempotency records.
56
- */
57
- async cleanup(expirationTimestamp: number): Promise<void> {
58
- await this.database.prepare(`DELETE FROM public_idempotency_keys WHERE expires_at < ?`).bind(expirationTimestamp).run()
59
- }
60
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { IdempotencyRepository, IdempotencyRecord } from '@beechcms/core'
6
+ import { BaseD1Repository } from './base.repository.d1.js'
7
+
8
+ export class D1IdempotencyRepository extends BaseD1Repository implements IdempotencyRepository {
9
+ /**
10
+ * Looks up an idempotency record by its key.
11
+ */
12
+ async lookup(idempotencyKey: string): Promise<IdempotencyRecord | null> {
13
+ const idempotencyRecordResult = await this.database.prepare(
14
+ `SELECT idempotency_key, request_fingerprint, response_status, response_body, expires_at
15
+ FROM public_idempotency_keys WHERE idempotency_key = ? LIMIT 1`
16
+ ).bind(idempotencyKey).first<any>()
17
+
18
+ if (!idempotencyRecordResult) {
19
+ return null
20
+ }
21
+
22
+ return {
23
+ key: idempotencyRecordResult.idempotency_key,
24
+ fingerprint: idempotencyRecordResult.request_fingerprint,
25
+ responseStatus: idempotencyRecordResult.response_status,
26
+ responseBody: idempotencyRecordResult.response_body,
27
+ expiresAt: idempotencyRecordResult.expires_at
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Stores or updates an idempotency record.
33
+ */
34
+ async store(idempotencyData: IdempotencyRecord): Promise<void> {
35
+ await this.database.prepare(
36
+ `INSERT INTO public_idempotency_keys (idempotency_key, request_fingerprint, response_status, response_body, created_at, expires_at)
37
+ VALUES (?, ?, ?, ?, ?, ?)
38
+ ON CONFLICT(idempotency_key) DO UPDATE SET
39
+ request_fingerprint = excluded.request_fingerprint,
40
+ response_status = excluded.response_status,
41
+ response_body = excluded.response_body,
42
+ created_at = excluded.created_at,
43
+ expires_at = excluded.expires_at`
44
+ ).bind(
45
+ idempotencyData.key,
46
+ idempotencyData.fingerprint,
47
+ idempotencyData.responseStatus,
48
+ idempotencyData.responseBody,
49
+ Math.floor(Date.now() / 1000),
50
+ idempotencyData.expiresAt
51
+ ).run()
52
+ }
53
+
54
+ /**
55
+ * Cleans up expired idempotency records.
56
+ */
57
+ async cleanup(expirationTimestamp: number): Promise<void> {
58
+ await this.database.prepare(`DELETE FROM public_idempotency_keys WHERE expires_at < ?`).bind(expirationTimestamp).run()
59
+ }
60
+ }
@@ -1,19 +1,19 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { IActivityLogger, ActivityLogEntry } from '@beechcms/core'
6
-
7
- /**
8
- * Test double for {@link IActivityLogger}.
9
- *
10
- * Captures every call into a public array so test assertions can verify
11
- * audit-trail side effects without touching D1. Preserves insertion order.
12
- */
13
- export class InMemoryActivityLogger implements IActivityLogger {
14
- public readonly entries: ActivityLogEntry[] = []
15
-
16
- log(entry: ActivityLogEntry): void {
17
- this.entries.push(entry)
18
- }
19
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import type { IActivityLogger, ActivityLogEntry } from '@beechcms/core'
6
+
7
+ /**
8
+ * Test double for {@link IActivityLogger}.
9
+ *
10
+ * Captures every call into a public array so test assertions can verify
11
+ * audit-trail side effects without touching D1. Preserves insertion order.
12
+ */
13
+ export class InMemoryActivityLogger implements IActivityLogger {
14
+ public readonly entries: ActivityLogEntry[] = []
15
+
16
+ log(entry: ActivityLogEntry): void {
17
+ this.entries.push(entry)
18
+ }
19
+ }
@@ -1,19 +1,19 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { INotificationService, CreateNotificationInput } from '@beechcms/core'
6
-
7
- /**
8
- * Test double for {@link INotificationService}.
9
- *
10
- * Captures every call into a public array so tests can assert what would have
11
- * been delivered without touching D1.
12
- */
13
- export class InMemoryNotificationService implements INotificationService {
14
- public readonly receivedNotifications: CreateNotificationInput[] = []
15
-
16
- notify(input: CreateNotificationInput): void {
17
- this.receivedNotifications.push(input)
18
- }
19
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import type { INotificationService, CreateNotificationInput } from '@beechcms/core'
6
+
7
+ /**
8
+ * Test double for {@link INotificationService}.
9
+ *
10
+ * Captures every call into a public array so tests can assert what would have
11
+ * been delivered without touching D1.
12
+ */
13
+ export class InMemoryNotificationService implements INotificationService {
14
+ public readonly receivedNotifications: CreateNotificationInput[] = []
15
+
16
+ notify(input: CreateNotificationInput): void {
17
+ this.receivedNotifications.push(input)
18
+ }
19
+ }
@@ -1,51 +1,58 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { ISeedRepository, Seed, SeedRecord } from '@beechcms/core'
6
-
7
- /**
8
- * In-memory ISeedRepository for testing and for the config.seeds back-compat shim.
9
- * Returns a fixed seed list with a static version token.
10
- */
11
- export class InMemorySeedRepository implements ISeedRepository {
12
- private readonly seeds: Seed[]
13
- private version = 1
14
-
15
- constructor(seeds: Seed[]) {
16
- this.seeds = seeds.filter(s => s && typeof s === 'object' && 'slug' in s)
17
- }
18
-
19
- async listActive(): Promise<Seed[]> {
20
- return this.seeds
21
- }
22
-
23
- async listAll(): Promise<SeedRecord[]> {
24
- return this.seeds.map(s => ({
25
- slug: s.slug,
26
- definition: s,
27
- status: 'active' as const,
28
- source: 'code' as const,
29
- createdAt: 0,
30
- updatedAt: 0,
31
- }))
32
- }
33
-
34
- async get(slug: string): Promise<SeedRecord | null> {
35
- const s = this.seeds.find(s => s.slug === slug)
36
- if (!s) return null
37
- return { slug: s.slug, definition: s, status: 'active', source: 'code', createdAt: 0, updatedAt: 0 }
38
- }
39
-
40
- async upsert(): Promise<void> {}
41
- async softDelete(): Promise<void> {}
42
- async hardDelete(): Promise<void> {}
43
-
44
- async getRegistryVersion(): Promise<number> {
45
- return this.version
46
- }
47
-
48
- async bumpRegistryVersion(): Promise<number> {
49
- return ++this.version
50
- }
51
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import type { ISeedRepository, Seed, SeedRecord } from '@beechcms/core'
6
+
7
+ /**
8
+ * In-memory ISeedRepository for testing and for the config.seeds back-compat shim.
9
+ * Returns a fixed seed list with a static version token.
10
+ */
11
+ export class InMemorySeedRepository implements ISeedRepository {
12
+ private readonly seeds: Seed[]
13
+ private version = 1
14
+
15
+ constructor(seeds: Seed[]) {
16
+ this.seeds = seeds.filter(s => s && typeof s === 'object' && 'slug' in s)
17
+ }
18
+
19
+ async listActive(): Promise<Seed[]> {
20
+ return this.seeds
21
+ }
22
+
23
+ async listAll(): Promise<SeedRecord[]> {
24
+ return this.seeds.map(s => ({
25
+ slug: s.slug,
26
+ definition: s,
27
+ status: 'active' as const,
28
+ source: 'code' as const,
29
+ createdAt: 0,
30
+ updatedAt: 0,
31
+ }))
32
+ }
33
+
34
+ async get(slug: string): Promise<SeedRecord | null> {
35
+ const s = this.seeds.find(s => s.slug === slug)
36
+ if (!s) return null
37
+ return { slug: s.slug, definition: s, status: 'active', source: 'code', createdAt: 0, updatedAt: 0 }
38
+ }
39
+ upsert(_slug: string, _definition: Seed, _source?: 'code' | 'runtime'): Promise<void> {
40
+ return Promise.resolve()
41
+ }
42
+
43
+ softDelete(_slug: string): Promise<void> {
44
+ return Promise.resolve()
45
+ }
46
+
47
+ hardDelete(_slug: string): Promise<void> {
48
+ return Promise.resolve()
49
+ }
50
+
51
+ async getRegistryVersion(): Promise<number> {
52
+ return this.version
53
+ }
54
+
55
+ async bumpRegistryVersion(): Promise<number> {
56
+ return ++this.version
57
+ }
58
+ }