@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.
- package/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/Searching.svg +1 -0
- package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
- package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
- package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
- package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/noResult.svg +43 -0
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/assets/dashboard/working.svg +1 -0
- package/migrations/0000_v040_base.sql +265 -238
- package/migrations/0029_automations.sql +14 -14
- package/migrations/0030_test_seeds.sql +125 -125
- package/package.json +13 -9
- package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +17 -13
- package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +22 -18
- package/src/auth/bcrypt-hash-provider.ts +24 -20
- package/src/auth/constants.ts +14 -10
- package/src/auth/generate-refresh-token.test.ts +23 -19
- package/src/auth/hash-provider.test.ts +55 -46
- package/src/auth/jose-token-service.ts +62 -55
- package/src/auth/jwt-claims-passthrough.test.ts +87 -0
- package/src/auth/login.test.ts +100 -92
- package/src/auth/login.ts +78 -74
- package/src/auth/refresh.ts +11 -5
- package/src/auth/token-service.test.ts +90 -82
- package/src/factory.ts +370 -347
- package/src/features/automations/__tests__/action-executors.test.ts +255 -268
- package/src/features/automations/__tests__/automation-runner.test.ts +196 -192
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -56
- package/src/features/automations/__tests__/automations.handler.test.ts +264 -260
- package/src/features/automations/__tests__/automations.repository.test.ts +163 -159
- package/src/features/automations/__tests__/automations.schema.test.ts +224 -134
- package/src/features/automations/__tests__/context-resolver.test.ts +126 -122
- package/src/features/automations/__tests__/cron-runner.test.ts +267 -263
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -140
- package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -306
- package/src/features/automations/__tests__/template-grammar.test.ts +308 -304
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
- package/src/features/automations/__tests__/when-evaluator.test.ts +274 -270
- package/src/features/automations/__tests__/when-pushdown.test.ts +281 -277
- package/src/features/automations/action-executors/create-entry.executor.ts +27 -23
- package/src/features/automations/action-executors/edit-field.executor.ts +26 -22
- package/src/features/automations/action-executors/index.ts +37 -33
- package/src/features/automations/action-executors/send-mail.executor.ts +53 -42
- package/src/features/automations/action-executors/set-variable.executor.ts +159 -146
- package/src/features/automations/action-executors/webhook.executor.ts +67 -25
- package/src/features/automations/automation-runner.ts +85 -81
- package/src/features/automations/automation-runner.utils.ts +47 -43
- package/src/features/automations/automations.handler.ts +197 -193
- package/src/features/automations/automations.schema.ts +177 -160
- package/src/features/automations/context-resolver.ts +152 -148
- package/src/features/automations/cron-runner.ts +140 -136
- package/src/features/automations/cron-runner.utils.ts +44 -40
- package/src/features/automations/filter-translation.ts +46 -42
- package/src/features/automations/index.ts +16 -12
- package/src/features/automations/template-grammar.ts +245 -241
- package/src/features/automations/var-access-resolver.ts +140 -136
- package/src/features/automations/when-evaluator.ts +87 -83
- package/src/features/automations/when-pushdown.ts +57 -53
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
- package/src/features/backrefs/backrefs.handler.ts +168 -0
- package/src/features/backrefs/d1-backref.repository.ts +84 -0
- package/src/features/backrefs/index.ts +5 -0
- package/src/features/content/constants.ts +16 -10
- package/src/features/content/handlers/bulk.handler.ts +185 -0
- package/src/features/content/handlers/create.ts +129 -165
- package/src/features/content/handlers/delete.ts +62 -87
- package/src/features/content/handlers/facets.ts +49 -45
- package/src/features/content/handlers/get.ts +120 -116
- package/src/features/content/handlers/helpers.test.ts +180 -0
- package/src/features/content/handlers/helpers.ts +93 -0
- package/src/features/content/handlers/list.ts +174 -88
- package/src/features/content/handlers/update.ts +174 -219
- package/src/features/content/index.ts +26 -20
- package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
- package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
- package/src/features/dashboard-layout/index.ts +5 -0
- package/src/features/draft/draft.handler.ts +195 -170
- package/src/features/draft/draft.middleware.ts +66 -62
- package/src/features/draft/index.ts +5 -1
- package/src/features/email/email.provider.ts +42 -38
- package/src/features/email/email.service.ts +98 -93
- package/src/features/email/email.types.ts +119 -108
- package/src/features/email/index.ts +33 -29
- package/src/features/email/providers/resend.ts +67 -63
- package/src/features/email/providers/smtp.ts +55 -0
- package/src/features/email/templates/automation-mail.ts +19 -15
- package/src/features/email/templates/password-changed.ts +63 -59
- package/src/features/email/templates/password-reset.ts +68 -64
- package/src/features/email/templates/shell.ts +96 -92
- package/src/features/notifications/index.ts +5 -1
- package/src/features/notifications/notifications.handler.ts +105 -101
- package/src/features/password-reset/index.ts +19 -15
- package/src/features/password-reset/request.ts +92 -82
- package/src/features/password-reset/reset.ts +102 -92
- package/src/features/rotate-field/index.ts +5 -1
- package/src/features/rotate-field/rotate-field.handler.ts +132 -128
- package/src/features/rotate-field/rotate-field.schema.ts +17 -13
- package/src/features/schema/schema.handler.ts +131 -16
- package/src/features/seeds/index.ts +5 -0
- package/src/features/seeds/seeds.handler.test.ts +1199 -0
- package/src/features/seeds/seeds.handler.ts +558 -0
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
- package/src/features/settings/settings.handler.ts +392 -302
- package/src/features/setup/index.ts +288 -91
- package/src/features/stats/index.ts +5 -1
- package/src/features/stats/stats.handler.ts +420 -430
- package/src/features/webhooks/index.ts +63 -0
- package/src/index.ts +65 -56
- package/src/media-utils.ts +82 -78
- package/src/middleware/auth-providers.middleware.test.ts +85 -0
- package/src/middleware/auth-providers.middleware.ts +45 -32
- package/src/middleware/observability.middleware.test.ts +93 -0
- package/src/middleware/observability.middleware.ts +70 -52
- package/src/middleware/rate-limit.middleware.ts +45 -41
- package/src/middleware/repository.middleware.ts +117 -91
- package/src/middleware/seed-registry.middleware.test.ts +97 -0
- package/src/middleware/seed-registry.middleware.ts +21 -0
- package/src/middleware/storage.middleware.ts +25 -21
- package/src/middleware.ts +45 -47
- package/src/public/access-policy.ts +27 -23
- package/src/public/api-key-middleware.ts +57 -53
- package/src/public/cache-utils.ts +38 -34
- package/src/public/entry-projection.ts +46 -42
- package/src/public/idempotency.ts +23 -19
- package/src/public/index.ts +16 -12
- package/src/public/problem-details.ts +124 -53
- package/src/public/public-add.ts +114 -110
- package/src/public/public-edit.ts +163 -159
- package/src/public/public-errors.ts +19 -15
- package/src/public/public-read.ts +63 -59
- package/src/public/public-routes.ts +88 -84
- package/src/public/query-builder.test.ts +224 -220
- package/src/public/query-builder.ts +156 -152
- package/src/public/rate-limit-middleware.ts +34 -30
- package/src/public/read-list.ts +54 -50
- package/src/public/read-single.ts +48 -44
- package/src/public/response-builder.ts +30 -26
- package/src/public/sanitize.ts +69 -65
- package/src/public/slug-utils.ts +18 -14
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -26
- package/src/rate-limit/cloudflare-rate-limiter.ts +15 -11
- package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -33
- package/src/rate-limit/in-memory-rate-limiter.ts +17 -13
- package/src/rate-limit/no-op-rate-limiter.test.ts +22 -18
- package/src/rate-limit/no-op-rate-limiter.ts +11 -7
- package/src/search-utils.test.ts +211 -207
- package/src/search-utils.ts +213 -209
- package/src/search.ts +65 -61
- package/src/shared/apply-policies.test.ts +81 -77
- package/src/shared/apply-policies.ts +67 -63
- package/src/shared/automations.repository.d1.ts +150 -146
- package/src/shared/background-notification-service.test.ts +62 -58
- package/src/shared/background-notification-service.ts +52 -48
- package/src/shared/base.repository.d1.ts +32 -28
- package/src/shared/content-utils.test.ts +165 -161
- package/src/shared/content-utils.ts +86 -82
- package/src/shared/content.repository.d1.test.ts +507 -312
- package/src/shared/content.repository.d1.ts +851 -382
- package/src/shared/d1-activity-log.repository.test.ts +140 -136
- package/src/shared/d1-activity-log.repository.ts +105 -101
- package/src/shared/d1-activity-logger.test.ts +86 -82
- package/src/shared/d1-activity-logger.ts +67 -63
- package/src/shared/d1-analytics.repository.test.ts +78 -74
- package/src/shared/d1-analytics.repository.ts +85 -81
- package/src/shared/d1-content-scan.repository.test.ts +80 -76
- package/src/shared/d1-content-scan.repository.ts +37 -29
- package/src/shared/d1-notification.repository.test.ts +128 -124
- package/src/shared/d1-notification.repository.ts +118 -114
- package/src/shared/d1-password-reset-token.repository.test.ts +81 -77
- package/src/shared/d1-password-reset-token.repository.ts +56 -52
- package/src/shared/d1-search.repository.test.ts +87 -83
- package/src/shared/d1-search.repository.ts +88 -84
- package/src/shared/d1-session.repository.test.ts +125 -121
- package/src/shared/d1-session.repository.ts +102 -98
- package/src/shared/d1-setup-checklist.repository.ts +36 -0
- package/src/shared/d1-user.repository.test.ts +151 -147
- package/src/shared/d1-user.repository.ts +119 -109
- package/src/shared/d1-widget.repository.test.ts +267 -217
- package/src/shared/d1-widget.repository.ts +385 -337
- package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
- package/src/shared/dashboard-layout.repository.d1.ts +58 -0
- package/src/shared/demo-data-sql.ts +216 -0
- package/src/shared/demo-data.repository.d1.ts +20 -0
- package/src/shared/execution-context-scheduler.ts +13 -9
- package/src/shared/fixed-clock.ts +25 -21
- package/src/shared/fts-sync.ts +8 -4
- package/src/shared/idempotency.repository.d1.test.ts +83 -79
- package/src/shared/idempotency.repository.d1.ts +60 -56
- package/src/shared/in-memory-activity-logger.ts +19 -15
- package/src/shared/in-memory-notification-service.ts +19 -15
- package/src/shared/in-memory-seed.repository.ts +58 -0
- package/src/shared/media.repository.d1.test.ts +107 -103
- package/src/shared/media.repository.d1.ts +68 -64
- package/src/shared/qstash-notification-service.test.ts +67 -0
- package/src/shared/qstash-notification-service.ts +55 -0
- package/src/shared/query-utils.ts +141 -137
- package/src/shared/request-utils.ts +26 -22
- package/src/shared/schema-mutator.d1.test.ts +142 -0
- package/src/shared/schema-mutator.d1.ts +64 -0
- package/src/shared/seed-layout.repository.d1.test.ts +114 -0
- package/src/shared/seed-layout.repository.d1.ts +62 -0
- package/src/shared/seed-registry-cache.test.ts +68 -0
- package/src/shared/seed-registry-cache.ts +49 -0
- package/src/shared/seed.repository.d1.test.ts +143 -0
- package/src/shared/seed.repository.d1.ts +98 -0
- package/src/shared/sequential-id-generator.ts +33 -22
- package/src/shared/site-settings.repository.d1.ts +53 -0
- package/src/shared/storage/factory.ts +41 -40
- package/src/shared/storage/s3-bucket.ts +195 -163
- package/src/shared/storage-utils.ts +40 -36
- package/src/shared/system-stats.repository.d1.test.ts +58 -54
- package/src/shared/system-stats.repository.d1.ts +48 -44
- package/src/types.ts +90 -67
- package/src/upload.ts +198 -187
- package/src/widget.test.ts +85 -0
- package/src/widget.ts +251 -208
- package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
- package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
- package/src/shared/storage/r2-binding-bucket.ts +0 -81
|
@@ -1,76 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
})
|