@beechcms/api 0.5.0 → 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 (225) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/Searching.svg +1 -0
  4. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  5. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  6. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  7. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  8. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  9. package/assets/dashboard/beechLogoDark.svg +48 -48
  10. package/assets/dashboard/index.html +18 -18
  11. package/assets/dashboard/noResult.svg +43 -0
  12. package/assets/dashboard/sol.svg +3 -3
  13. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  14. package/assets/dashboard/working.svg +1 -0
  15. package/migrations/0000_v040_base.sql +265 -238
  16. package/migrations/0029_automations.sql +14 -14
  17. package/migrations/0030_test_seeds.sql +125 -125
  18. package/package.json +13 -9
  19. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +17 -13
  20. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +22 -18
  21. package/src/auth/bcrypt-hash-provider.ts +24 -20
  22. package/src/auth/constants.ts +14 -10
  23. package/src/auth/generate-refresh-token.test.ts +23 -19
  24. package/src/auth/hash-provider.test.ts +55 -46
  25. package/src/auth/jose-token-service.ts +62 -55
  26. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  27. package/src/auth/login.test.ts +100 -92
  28. package/src/auth/login.ts +78 -74
  29. package/src/auth/refresh.ts +11 -5
  30. package/src/auth/token-service.test.ts +90 -82
  31. package/src/factory.ts +370 -347
  32. package/src/features/automations/__tests__/action-executors.test.ts +255 -268
  33. package/src/features/automations/__tests__/automation-runner.test.ts +196 -192
  34. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -56
  35. package/src/features/automations/__tests__/automations.handler.test.ts +264 -260
  36. package/src/features/automations/__tests__/automations.repository.test.ts +163 -159
  37. package/src/features/automations/__tests__/automations.schema.test.ts +224 -134
  38. package/src/features/automations/__tests__/context-resolver.test.ts +126 -122
  39. package/src/features/automations/__tests__/cron-runner.test.ts +267 -263
  40. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -140
  41. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -306
  42. package/src/features/automations/__tests__/template-grammar.test.ts +308 -304
  43. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  44. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -270
  45. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -277
  46. package/src/features/automations/action-executors/create-entry.executor.ts +27 -23
  47. package/src/features/automations/action-executors/edit-field.executor.ts +26 -22
  48. package/src/features/automations/action-executors/index.ts +37 -33
  49. package/src/features/automations/action-executors/send-mail.executor.ts +53 -42
  50. package/src/features/automations/action-executors/set-variable.executor.ts +159 -146
  51. package/src/features/automations/action-executors/webhook.executor.ts +67 -25
  52. package/src/features/automations/automation-runner.ts +85 -81
  53. package/src/features/automations/automation-runner.utils.ts +47 -43
  54. package/src/features/automations/automations.handler.ts +197 -193
  55. package/src/features/automations/automations.schema.ts +177 -160
  56. package/src/features/automations/context-resolver.ts +152 -148
  57. package/src/features/automations/cron-runner.ts +140 -136
  58. package/src/features/automations/cron-runner.utils.ts +44 -40
  59. package/src/features/automations/filter-translation.ts +46 -42
  60. package/src/features/automations/index.ts +16 -12
  61. package/src/features/automations/template-grammar.ts +245 -241
  62. package/src/features/automations/var-access-resolver.ts +140 -136
  63. package/src/features/automations/when-evaluator.ts +87 -83
  64. package/src/features/automations/when-pushdown.ts +57 -53
  65. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  66. package/src/features/backrefs/backrefs.handler.ts +168 -0
  67. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  68. package/src/features/backrefs/index.ts +5 -0
  69. package/src/features/content/constants.ts +16 -10
  70. package/src/features/content/handlers/bulk.handler.ts +185 -0
  71. package/src/features/content/handlers/create.ts +129 -165
  72. package/src/features/content/handlers/delete.ts +62 -87
  73. package/src/features/content/handlers/facets.ts +49 -45
  74. package/src/features/content/handlers/get.ts +120 -116
  75. package/src/features/content/handlers/helpers.test.ts +180 -0
  76. package/src/features/content/handlers/helpers.ts +93 -0
  77. package/src/features/content/handlers/list.ts +174 -88
  78. package/src/features/content/handlers/update.ts +174 -219
  79. package/src/features/content/index.ts +26 -20
  80. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  81. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  82. package/src/features/dashboard-layout/index.ts +5 -0
  83. package/src/features/draft/draft.handler.ts +195 -170
  84. package/src/features/draft/draft.middleware.ts +66 -62
  85. package/src/features/draft/index.ts +5 -1
  86. package/src/features/email/email.provider.ts +42 -38
  87. package/src/features/email/email.service.ts +98 -93
  88. package/src/features/email/email.types.ts +119 -108
  89. package/src/features/email/index.ts +33 -29
  90. package/src/features/email/providers/resend.ts +67 -63
  91. package/src/features/email/providers/smtp.ts +55 -0
  92. package/src/features/email/templates/automation-mail.ts +19 -15
  93. package/src/features/email/templates/password-changed.ts +63 -59
  94. package/src/features/email/templates/password-reset.ts +68 -64
  95. package/src/features/email/templates/shell.ts +96 -92
  96. package/src/features/notifications/index.ts +5 -1
  97. package/src/features/notifications/notifications.handler.ts +105 -101
  98. package/src/features/password-reset/index.ts +19 -15
  99. package/src/features/password-reset/request.ts +92 -82
  100. package/src/features/password-reset/reset.ts +102 -92
  101. package/src/features/rotate-field/index.ts +5 -1
  102. package/src/features/rotate-field/rotate-field.handler.ts +132 -128
  103. package/src/features/rotate-field/rotate-field.schema.ts +17 -13
  104. package/src/features/schema/schema.handler.ts +131 -16
  105. package/src/features/seeds/index.ts +5 -0
  106. package/src/features/seeds/seeds.handler.test.ts +1199 -0
  107. package/src/features/seeds/seeds.handler.ts +558 -0
  108. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  109. package/src/features/settings/settings.handler.ts +392 -302
  110. package/src/features/setup/index.ts +288 -91
  111. package/src/features/stats/index.ts +5 -1
  112. package/src/features/stats/stats.handler.ts +420 -430
  113. package/src/features/webhooks/index.ts +63 -0
  114. package/src/index.ts +65 -56
  115. package/src/media-utils.ts +82 -78
  116. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  117. package/src/middleware/auth-providers.middleware.ts +45 -32
  118. package/src/middleware/observability.middleware.test.ts +93 -0
  119. package/src/middleware/observability.middleware.ts +70 -52
  120. package/src/middleware/rate-limit.middleware.ts +45 -41
  121. package/src/middleware/repository.middleware.ts +117 -91
  122. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  123. package/src/middleware/seed-registry.middleware.ts +21 -0
  124. package/src/middleware/storage.middleware.ts +25 -21
  125. package/src/middleware.ts +45 -47
  126. package/src/public/access-policy.ts +27 -23
  127. package/src/public/api-key-middleware.ts +57 -53
  128. package/src/public/cache-utils.ts +38 -34
  129. package/src/public/entry-projection.ts +46 -42
  130. package/src/public/idempotency.ts +23 -19
  131. package/src/public/index.ts +16 -12
  132. package/src/public/problem-details.ts +124 -53
  133. package/src/public/public-add.ts +114 -110
  134. package/src/public/public-edit.ts +163 -159
  135. package/src/public/public-errors.ts +19 -15
  136. package/src/public/public-read.ts +63 -59
  137. package/src/public/public-routes.ts +88 -84
  138. package/src/public/query-builder.test.ts +224 -220
  139. package/src/public/query-builder.ts +156 -152
  140. package/src/public/rate-limit-middleware.ts +34 -30
  141. package/src/public/read-list.ts +54 -50
  142. package/src/public/read-single.ts +48 -44
  143. package/src/public/response-builder.ts +30 -26
  144. package/src/public/sanitize.ts +69 -65
  145. package/src/public/slug-utils.ts +18 -14
  146. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -26
  147. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -11
  148. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -33
  149. package/src/rate-limit/in-memory-rate-limiter.ts +17 -13
  150. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -18
  151. package/src/rate-limit/no-op-rate-limiter.ts +11 -7
  152. package/src/search-utils.test.ts +211 -207
  153. package/src/search-utils.ts +213 -209
  154. package/src/search.ts +65 -61
  155. package/src/shared/apply-policies.test.ts +81 -77
  156. package/src/shared/apply-policies.ts +67 -63
  157. package/src/shared/automations.repository.d1.ts +150 -146
  158. package/src/shared/background-notification-service.test.ts +62 -58
  159. package/src/shared/background-notification-service.ts +52 -48
  160. package/src/shared/base.repository.d1.ts +32 -28
  161. package/src/shared/content-utils.test.ts +165 -161
  162. package/src/shared/content-utils.ts +86 -82
  163. package/src/shared/content.repository.d1.test.ts +507 -312
  164. package/src/shared/content.repository.d1.ts +851 -382
  165. package/src/shared/d1-activity-log.repository.test.ts +140 -136
  166. package/src/shared/d1-activity-log.repository.ts +105 -101
  167. package/src/shared/d1-activity-logger.test.ts +86 -82
  168. package/src/shared/d1-activity-logger.ts +67 -63
  169. package/src/shared/d1-analytics.repository.test.ts +78 -74
  170. package/src/shared/d1-analytics.repository.ts +85 -81
  171. package/src/shared/d1-content-scan.repository.test.ts +80 -76
  172. package/src/shared/d1-content-scan.repository.ts +37 -29
  173. package/src/shared/d1-notification.repository.test.ts +128 -124
  174. package/src/shared/d1-notification.repository.ts +118 -114
  175. package/src/shared/d1-password-reset-token.repository.test.ts +81 -77
  176. package/src/shared/d1-password-reset-token.repository.ts +56 -52
  177. package/src/shared/d1-search.repository.test.ts +87 -83
  178. package/src/shared/d1-search.repository.ts +88 -84
  179. package/src/shared/d1-session.repository.test.ts +125 -121
  180. package/src/shared/d1-session.repository.ts +102 -98
  181. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  182. package/src/shared/d1-user.repository.test.ts +151 -147
  183. package/src/shared/d1-user.repository.ts +119 -109
  184. package/src/shared/d1-widget.repository.test.ts +267 -217
  185. package/src/shared/d1-widget.repository.ts +385 -337
  186. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  187. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  188. package/src/shared/demo-data-sql.ts +216 -0
  189. package/src/shared/demo-data.repository.d1.ts +20 -0
  190. package/src/shared/execution-context-scheduler.ts +13 -9
  191. package/src/shared/fixed-clock.ts +25 -21
  192. package/src/shared/fts-sync.ts +8 -4
  193. package/src/shared/idempotency.repository.d1.test.ts +83 -79
  194. package/src/shared/idempotency.repository.d1.ts +60 -56
  195. package/src/shared/in-memory-activity-logger.ts +19 -15
  196. package/src/shared/in-memory-notification-service.ts +19 -15
  197. package/src/shared/in-memory-seed.repository.ts +58 -0
  198. package/src/shared/media.repository.d1.test.ts +107 -103
  199. package/src/shared/media.repository.d1.ts +68 -64
  200. package/src/shared/qstash-notification-service.test.ts +67 -0
  201. package/src/shared/qstash-notification-service.ts +55 -0
  202. package/src/shared/query-utils.ts +141 -137
  203. package/src/shared/request-utils.ts +26 -22
  204. package/src/shared/schema-mutator.d1.test.ts +142 -0
  205. package/src/shared/schema-mutator.d1.ts +64 -0
  206. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  207. package/src/shared/seed-layout.repository.d1.ts +62 -0
  208. package/src/shared/seed-registry-cache.test.ts +68 -0
  209. package/src/shared/seed-registry-cache.ts +49 -0
  210. package/src/shared/seed.repository.d1.test.ts +143 -0
  211. package/src/shared/seed.repository.d1.ts +98 -0
  212. package/src/shared/sequential-id-generator.ts +33 -22
  213. package/src/shared/site-settings.repository.d1.ts +53 -0
  214. package/src/shared/storage/factory.ts +41 -40
  215. package/src/shared/storage/s3-bucket.ts +195 -163
  216. package/src/shared/storage-utils.ts +40 -36
  217. package/src/shared/system-stats.repository.d1.test.ts +58 -54
  218. package/src/shared/system-stats.repository.d1.ts +48 -44
  219. package/src/types.ts +90 -67
  220. package/src/upload.ts +198 -187
  221. package/src/widget.test.ts +85 -0
  222. package/src/widget.ts +251 -208
  223. package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
  224. package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
  225. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -1,76 +1,80 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import type { Seed } from '@beechcms/core'
3
- import { D1ContentScanRepository } from './d1-content-scan.repository'
4
-
5
- function makeMockDb(allResults: unknown[] = []) {
6
- const allMock = vi.fn().mockResolvedValue({ results: allResults })
7
- const prepareMock = vi.fn(() => ({ all: allMock }))
8
- return { db: { prepare: prepareMock } as any, prepareMock, allMock }
9
- }
10
-
11
- describe('D1ContentScanRepository', () => {
12
- it('returns empty set if seeds list is empty', async () => {
13
- const { db, prepareMock } = makeMockDb()
14
- const repo = new D1ContentScanRepository(db)
15
- const result = await repo.getReferencedMediaKeys([])
16
- expect(result.size).toBe(0)
17
- expect(prepareMock).not.toHaveBeenCalled()
18
- })
19
-
20
- it('skips seeds with no file branches', async () => {
21
- const { db, prepareMock } = makeMockDb()
22
- const repo = new D1ContentScanRepository(db)
23
- const seedWithoutFile: Seed = {
24
- slug: 'posts',
25
- label: 'Post',
26
- displayNameAlias: 'title',
27
- branches: [
28
- { alias: 'title', type: 'text', label: 'Title' },
29
- ],
30
- }
31
- const result = await repo.getReferencedMediaKeys([seedWithoutFile])
32
- expect(result.size).toBe(0)
33
- expect(prepareMock).not.toHaveBeenCalled()
34
- })
35
-
36
- it('extracts referenced media keys from file columns', async () => {
37
- const mockResults = [
38
- { cover: '/api/media/image1.jpg', gallery: '/api/media/img2.png /api/media/img3%20spaced.jpg' },
39
- { cover: null, gallery: 'no-media-here' },
40
- ]
41
- const { db, prepareMock } = makeMockDb(mockResults)
42
- const repo = new D1ContentScanRepository(db)
43
- const seedWithFiles: Seed = {
44
- slug: 'articles',
45
- label: 'Article',
46
- displayNameAlias: 'title',
47
- branches: [
48
- { alias: 'title', type: 'text', label: 'Title' },
49
- { alias: 'cover', type: 'file', label: 'Cover' },
50
- { alias: 'gallery', type: 'file', label: 'Gallery' },
51
- ],
52
- }
53
-
54
- const result = await repo.getReferencedMediaKeys([seedWithFiles])
55
- expect(prepareMock).toHaveBeenCalledWith('SELECT cover, gallery FROM content_articles')
56
- expect(result).toEqual(new Set(['image1.jpg', 'img2.png', 'img3 spaced.jpg']))
57
- })
58
-
59
- it('handles null or missing results gracefully', async () => {
60
- const allMock = vi.fn().mockResolvedValue({ results: null })
61
- const prepareMock = vi.fn(() => ({ all: allMock }))
62
- const db = { prepare: prepareMock } as any
63
- const repo = new D1ContentScanRepository(db)
64
- const seedWithFiles: Seed = {
65
- slug: 'articles',
66
- label: 'Article',
67
- displayNameAlias: 'title',
68
- branches: [
69
- { alias: 'cover', type: 'file', label: 'Cover' },
70
- ],
71
- }
72
-
73
- const result = await repo.getReferencedMediaKeys([seedWithFiles])
74
- expect(result.size).toBe(0)
75
- })
76
- })
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 type { Seed } from '@beechcms/core'
7
+ import { D1ContentScanRepository } from './d1-content-scan.repository'
8
+
9
+ function makeMockDb(allResults: unknown[] = []) {
10
+ const allMock = vi.fn().mockResolvedValue({ results: allResults })
11
+ const prepareMock = vi.fn(() => ({ all: allMock }))
12
+ return { db: { prepare: prepareMock } as any, prepareMock, allMock }
13
+ }
14
+
15
+ describe('D1ContentScanRepository', () => {
16
+ it('returns empty set if seeds list is empty', async () => {
17
+ const { db, prepareMock } = makeMockDb()
18
+ const repo = new D1ContentScanRepository(db)
19
+ const result = await repo.getReferencedMediaKeys([])
20
+ expect(result.size).toBe(0)
21
+ expect(prepareMock).not.toHaveBeenCalled()
22
+ })
23
+
24
+ it('skips seeds with no file branches', async () => {
25
+ const { db, prepareMock } = makeMockDb()
26
+ const repo = new D1ContentScanRepository(db)
27
+ const seedWithoutFile: Seed = {
28
+ slug: 'posts',
29
+ label: 'Post',
30
+ displayNameAlias: 'title',
31
+ branches: [
32
+ { id: 'br_01', alias: 'title', type: 'text', label: 'Title' },
33
+ ],
34
+ }
35
+ const result = await repo.getReferencedMediaKeys([seedWithoutFile])
36
+ expect(result.size).toBe(0)
37
+ expect(prepareMock).not.toHaveBeenCalled()
38
+ })
39
+
40
+ it('extracts referenced media keys from file columns', async () => {
41
+ const mockResults = [
42
+ { cover: '/api/media/image1.jpg', gallery: '/api/media/img2.png /api/media/img3%20spaced.jpg' },
43
+ { cover: null, gallery: 'no-media-here' },
44
+ ]
45
+ const { db, prepareMock } = makeMockDb(mockResults)
46
+ const repo = new D1ContentScanRepository(db)
47
+ const seedWithFiles: Seed = {
48
+ slug: 'articles',
49
+ label: 'Article',
50
+ displayNameAlias: 'title',
51
+ branches: [
52
+ { id: 'br_01', alias: 'title', type: 'text', label: 'Title' },
53
+ { id: 'br_02', alias: 'cover', type: 'file', label: 'Cover' },
54
+ { id: 'br_03', alias: 'gallery', type: 'file', label: 'Gallery' },
55
+ ],
56
+ }
57
+
58
+ const result = await repo.getReferencedMediaKeys([seedWithFiles])
59
+ expect(prepareMock).toHaveBeenCalledWith('SELECT cover, gallery FROM content_articles')
60
+ expect(result).toEqual(new Set(['image1.jpg', 'img2.png', 'img3 spaced.jpg']))
61
+ })
62
+
63
+ it('handles null or missing results gracefully', async () => {
64
+ const allMock = vi.fn().mockResolvedValue({ results: null })
65
+ const prepareMock = vi.fn(() => ({ all: allMock }))
66
+ const db = { prepare: prepareMock } as any
67
+ const repo = new D1ContentScanRepository(db)
68
+ const seedWithFiles: Seed = {
69
+ slug: 'articles',
70
+ label: 'Article',
71
+ displayNameAlias: 'title',
72
+ branches: [
73
+ { id: 'br_01', alias: 'cover', type: 'file', label: 'Cover' },
74
+ ],
75
+ }
76
+
77
+ const result = await repo.getReferencedMediaKeys([seedWithFiles])
78
+ expect(result.size).toBe(0)
79
+ })
80
+ })
@@ -1,29 +1,37 @@
1
- import type { D1Database } from '@cloudflare/workers-types'
2
- import type { IContentScanRepository, Seed } from '@beechcms/core'
3
-
4
- export class D1ContentScanRepository implements IContentScanRepository {
5
- constructor(private readonly db: D1Database) {}
6
-
7
- async getReferencedMediaKeys(seeds: Seed[]): Promise<Set<string>> {
8
- const referencedMediaKeys = new Set<string>()
9
-
10
- for (const seed of seeds) {
11
- const mediaFields = seed.branches.filter(branch => branch.type === 'file')
12
- if (mediaFields.length === 0) continue
13
-
14
- const mediaColumns = mediaFields.map(field => field.alias).join(', ')
15
- const contentData = await this.db.prepare(
16
- `SELECT ${mediaColumns} FROM content_${seed.slug}`
17
- ).all<Record<string, string | null>>()
18
-
19
- for (const contentRow of contentData.results ?? []) {
20
- const rowContentString = Object.values(contentRow).filter(Boolean).join(' ')
21
- for (const keyMatch of rowContentString.matchAll(/\/api\/media\/([^"'\s\\,}\]]+)/g)) {
22
- referencedMediaKeys.add(decodeURIComponent(keyMatch[1]))
23
- }
24
- }
25
- }
26
-
27
- return referencedMediaKeys
28
- }
29
- }
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 { D1Database } from '@cloudflare/workers-types'
6
+ import type { IContentScanRepository, Seed } from '@beechcms/core'
7
+
8
+ export class D1ContentScanRepository implements IContentScanRepository {
9
+ constructor(private readonly db: D1Database) {}
10
+
11
+ async getReferencedMediaKeys(seeds: Seed[]): Promise<Set<string>> {
12
+ const referencedMediaKeys = new Set<string>()
13
+
14
+ for (const seed of seeds) {
15
+ const mediaFields = seed.branches.filter(branch => branch.type === 'file')
16
+ if (mediaFields.length === 0) continue
17
+
18
+ const mediaColumns = mediaFields.map(field => field.alias).join(', ')
19
+ try {
20
+ const contentData = await this.db.prepare(
21
+ `SELECT ${mediaColumns} FROM content_${seed.slug}`
22
+ ).all<Record<string, string | null>>()
23
+
24
+ for (const contentRow of contentData.results ?? []) {
25
+ const rowContentString = Object.values(contentRow).filter(Boolean).join(' ')
26
+ for (const keyMatch of rowContentString.matchAll(/\/api\/media\/([^"'\s\\,}\]]+)/g)) {
27
+ referencedMediaKeys.add(decodeURIComponent(keyMatch[1]))
28
+ }
29
+ }
30
+ } catch {
31
+ // Table not yet created (seed:load not run) — skip this seed gracefully
32
+ }
33
+ }
34
+
35
+ return referencedMediaKeys
36
+ }
37
+ }
@@ -1,124 +1,128 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import { D1NotificationRepository } from './d1-notification.repository'
3
- import { FixedClock } from './fixed-clock'
4
- import { SequentialIdGenerator } from './sequential-id-generator'
5
-
6
- const clock = new FixedClock(1700000000_000)
7
- const makeIdGen = () => new SequentialIdGenerator()
8
-
9
- function makeMockDb(opts: { firstResult?: unknown; allResults?: unknown[] } = {}) {
10
- const { firstResult = null, allResults = [] } = opts
11
- const runMock = vi.fn().mockResolvedValue({ success: true })
12
- const firstMock = vi.fn().mockResolvedValue(firstResult)
13
- const allMock = vi.fn().mockResolvedValue({ results: allResults })
14
- const bindMock = vi.fn(() => ({ first: firstMock, all: allMock, run: runMock }))
15
- const prepareMock = vi.fn(() => ({
16
- bind: bindMock,
17
- first: firstMock,
18
- all: allMock,
19
- run: runMock,
20
- }))
21
- return { db: { prepare: prepareMock } as any, prepareMock, bindMock, runMock, firstMock, allMock }
22
- }
23
-
24
- describe('D1NotificationRepository', () => {
25
- describe('list', () => {
26
- it('maps rows to NotificationRecord with isRead boolean', async () => {
27
- const { db } = makeMockDb({
28
- allResults: [
29
- {
30
- id: 'n-1',
31
- title: 'T',
32
- message: 'M',
33
- type: 'success',
34
- is_read: 0,
35
- created_at: 100,
36
- },
37
- {
38
- id: 'n-2',
39
- title: 'T2',
40
- message: 'M2',
41
- type: 'info',
42
- is_read: 1,
43
- created_at: 99,
44
- },
45
- ],
46
- })
47
- const records = await new D1NotificationRepository(db, clock, makeIdGen()).list(10)
48
- expect(records).toEqual([
49
- { id: 'n-1', title: 'T', message: 'M', type: 'success', isRead: false, createdAt: 100 },
50
- { id: 'n-2', title: 'T2', message: 'M2', type: 'info', isRead: true, createdAt: 99 },
51
- ])
52
- })
53
-
54
- it('orders by created_at DESC with LIMIT bound', async () => {
55
- const { db, prepareMock, bindMock } = makeMockDb()
56
- await new D1NotificationRepository(db, clock, makeIdGen()).list(25)
57
- expect(prepareMock.mock.calls[0][0]).toMatch(/ORDER BY created_at DESC/)
58
- expect(bindMock).toHaveBeenCalledWith(25)
59
- })
60
- })
61
-
62
- describe('stats', () => {
63
- it('returns aggregate counters mapped to camelCase', async () => {
64
- const { db } = makeMockDb({
65
- firstResult: { total_count: 7, latest_created_at: 1234, read_count: 3 },
66
- })
67
- const stats = await new D1NotificationRepository(db, clock, makeIdGen()).stats()
68
- expect(stats).toEqual({ totalCount: 7, latestCreatedAt: 1234, readCount: 3 })
69
- })
70
-
71
- it('returns zeros when the table is empty', async () => {
72
- const { db } = makeMockDb({
73
- firstResult: { total_count: 0, latest_created_at: null, read_count: null },
74
- })
75
- const stats = await new D1NotificationRepository(db, clock, makeIdGen()).stats()
76
- expect(stats).toEqual({ totalCount: 0, latestCreatedAt: 0, readCount: 0 })
77
- })
78
- })
79
-
80
- describe('create', () => {
81
- it('inserts a new notification and returns the generated id', async () => {
82
- const { db, prepareMock, bindMock } = makeMockDb()
83
- const id = await new D1NotificationRepository(db, clock, makeIdGen()).create({
84
- title: 'Hello',
85
- message: 'World',
86
- type: 'info',
87
- })
88
- expect(typeof id).toBe('string')
89
- expect(id.length).toBeGreaterThan(0)
90
- expect(prepareMock.mock.calls[0][0]).toMatch(/INSERT INTO notifications/)
91
- expect(bindMock).toHaveBeenCalledWith(id, 'Hello', 'World', 'info')
92
- })
93
- })
94
-
95
- describe('markRead / markUnread / delete / markAllRead', () => {
96
- it('markRead binds the given id', async () => {
97
- const { db, prepareMock, bindMock } = makeMockDb()
98
- await new D1NotificationRepository(db, clock, makeIdGen()).markRead('n-1')
99
- expect(prepareMock.mock.calls[0][0]).toMatch(/UPDATE notifications SET is_read = 1 WHERE id = \?/)
100
- expect(bindMock).toHaveBeenCalledWith('n-1')
101
- })
102
-
103
- it('markUnread binds the given id', async () => {
104
- const { db, prepareMock, bindMock } = makeMockDb()
105
- await new D1NotificationRepository(db, clock, makeIdGen()).markUnread('n-2')
106
- expect(prepareMock.mock.calls[0][0]).toMatch(/UPDATE notifications SET is_read = 0 WHERE id = \?/)
107
- expect(bindMock).toHaveBeenCalledWith('n-2')
108
- })
109
-
110
- it('delete binds the given id', async () => {
111
- const { db, prepareMock, bindMock } = makeMockDb()
112
- await new D1NotificationRepository(db, clock, makeIdGen()).delete('n-3')
113
- expect(prepareMock.mock.calls[0][0]).toMatch(/DELETE FROM notifications WHERE id = \?/)
114
- expect(bindMock).toHaveBeenCalledWith('n-3')
115
- })
116
-
117
- it('markAllRead runs without bindings', async () => {
118
- const { db, prepareMock, runMock } = makeMockDb()
119
- await new D1NotificationRepository(db, clock, makeIdGen()).markAllRead()
120
- expect(prepareMock.mock.calls[0][0]).toMatch(/^UPDATE notifications SET is_read = 1$/)
121
- expect(runMock).toHaveBeenCalled()
122
- })
123
- })
124
- })
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 { D1NotificationRepository } from './d1-notification.repository'
7
+ import { FixedClock } from './fixed-clock'
8
+ import { SequentialIdGenerator } from './sequential-id-generator'
9
+
10
+ const clock = new FixedClock(1700000000_000)
11
+ const makeIdGen = () => new SequentialIdGenerator()
12
+
13
+ function makeMockDb(opts: { firstResult?: unknown; allResults?: unknown[] } = {}) {
14
+ const { firstResult = null, allResults = [] } = opts
15
+ const runMock = vi.fn().mockResolvedValue({ success: true })
16
+ const firstMock = vi.fn().mockResolvedValue(firstResult)
17
+ const allMock = vi.fn().mockResolvedValue({ results: allResults })
18
+ const bindMock = vi.fn(() => ({ first: firstMock, all: allMock, run: runMock }))
19
+ const prepareMock = vi.fn(() => ({
20
+ bind: bindMock,
21
+ first: firstMock,
22
+ all: allMock,
23
+ run: runMock,
24
+ }))
25
+ return { db: { prepare: prepareMock } as any, prepareMock, bindMock, runMock, firstMock, allMock }
26
+ }
27
+
28
+ describe('D1NotificationRepository', () => {
29
+ describe('list', () => {
30
+ it('maps rows to NotificationRecord with isRead boolean', async () => {
31
+ const { db } = makeMockDb({
32
+ allResults: [
33
+ {
34
+ id: 'n-1',
35
+ title: 'T',
36
+ message: 'M',
37
+ type: 'success',
38
+ is_read: 0,
39
+ created_at: 100,
40
+ },
41
+ {
42
+ id: 'n-2',
43
+ title: 'T2',
44
+ message: 'M2',
45
+ type: 'info',
46
+ is_read: 1,
47
+ created_at: 99,
48
+ },
49
+ ],
50
+ })
51
+ const records = await new D1NotificationRepository(db, clock, makeIdGen()).list(10)
52
+ expect(records).toEqual([
53
+ { id: 'n-1', title: 'T', message: 'M', type: 'success', isRead: false, createdAt: 100 },
54
+ { id: 'n-2', title: 'T2', message: 'M2', type: 'info', isRead: true, createdAt: 99 },
55
+ ])
56
+ })
57
+
58
+ it('orders by created_at DESC with LIMIT bound', async () => {
59
+ const { db, prepareMock, bindMock } = makeMockDb()
60
+ await new D1NotificationRepository(db, clock, makeIdGen()).list(25)
61
+ expect(prepareMock.mock.calls[0][0]).toMatch(/ORDER BY created_at DESC/)
62
+ expect(bindMock).toHaveBeenCalledWith(25)
63
+ })
64
+ })
65
+
66
+ describe('stats', () => {
67
+ it('returns aggregate counters mapped to camelCase', async () => {
68
+ const { db } = makeMockDb({
69
+ firstResult: { total_count: 7, latest_created_at: 1234, read_count: 3 },
70
+ })
71
+ const stats = await new D1NotificationRepository(db, clock, makeIdGen()).stats()
72
+ expect(stats).toEqual({ totalCount: 7, latestCreatedAt: 1234, readCount: 3 })
73
+ })
74
+
75
+ it('returns zeros when the table is empty', async () => {
76
+ const { db } = makeMockDb({
77
+ firstResult: { total_count: 0, latest_created_at: null, read_count: null },
78
+ })
79
+ const stats = await new D1NotificationRepository(db, clock, makeIdGen()).stats()
80
+ expect(stats).toEqual({ totalCount: 0, latestCreatedAt: 0, readCount: 0 })
81
+ })
82
+ })
83
+
84
+ describe('create', () => {
85
+ it('inserts a new notification and returns the generated id', async () => {
86
+ const { db, prepareMock, bindMock } = makeMockDb()
87
+ const id = await new D1NotificationRepository(db, clock, makeIdGen()).create({
88
+ title: 'Hello',
89
+ message: 'World',
90
+ type: 'info',
91
+ })
92
+ expect(typeof id).toBe('string')
93
+ expect(id.length).toBeGreaterThan(0)
94
+ expect(prepareMock.mock.calls[0][0]).toMatch(/INSERT INTO notifications/)
95
+ expect(bindMock).toHaveBeenCalledWith(id, 'Hello', 'World', 'info')
96
+ })
97
+ })
98
+
99
+ describe('markRead / markUnread / delete / markAllRead', () => {
100
+ it('markRead binds the given id', async () => {
101
+ const { db, prepareMock, bindMock } = makeMockDb()
102
+ await new D1NotificationRepository(db, clock, makeIdGen()).markRead('n-1')
103
+ expect(prepareMock.mock.calls[0][0]).toMatch(/UPDATE notifications SET is_read = 1 WHERE id = \?/)
104
+ expect(bindMock).toHaveBeenCalledWith('n-1')
105
+ })
106
+
107
+ it('markUnread binds the given id', async () => {
108
+ const { db, prepareMock, bindMock } = makeMockDb()
109
+ await new D1NotificationRepository(db, clock, makeIdGen()).markUnread('n-2')
110
+ expect(prepareMock.mock.calls[0][0]).toMatch(/UPDATE notifications SET is_read = 0 WHERE id = \?/)
111
+ expect(bindMock).toHaveBeenCalledWith('n-2')
112
+ })
113
+
114
+ it('delete binds the given id', async () => {
115
+ const { db, prepareMock, bindMock } = makeMockDb()
116
+ await new D1NotificationRepository(db, clock, makeIdGen()).delete('n-3')
117
+ expect(prepareMock.mock.calls[0][0]).toMatch(/DELETE FROM notifications WHERE id = \?/)
118
+ expect(bindMock).toHaveBeenCalledWith('n-3')
119
+ })
120
+
121
+ it('markAllRead runs without bindings', async () => {
122
+ const { db, prepareMock, runMock } = makeMockDb()
123
+ await new D1NotificationRepository(db, clock, makeIdGen()).markAllRead()
124
+ expect(prepareMock.mock.calls[0][0]).toMatch(/^UPDATE notifications SET is_read = 1$/)
125
+ expect(runMock).toHaveBeenCalled()
126
+ })
127
+ })
128
+ })