@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.
- package/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- 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 +42 -42
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +265 -265
- package/migrations/0029_automations.sql +14 -14
- package/migrations/0030_test_seeds.sql +125 -125
- package/package.json +2 -2
- package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
- package/src/auth/__fixtures__/static-token-service.ts +22 -22
- package/src/auth/bcrypt-hash-provider.ts +24 -24
- package/src/auth/constants.ts +14 -14
- package/src/auth/generate-refresh-token.test.ts +23 -23
- package/src/auth/hash-provider.test.ts +55 -55
- package/src/auth/jose-token-service.ts +62 -62
- package/src/auth/jwt-claims-passthrough.test.ts +87 -87
- package/src/auth/login.test.ts +100 -100
- package/src/auth/login.ts +78 -78
- package/src/auth/refresh.ts +11 -11
- package/src/auth/token-service.test.ts +90 -90
- package/src/factory.ts +370 -368
- package/src/features/automations/__tests__/action-executors.test.ts +255 -255
- package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
- package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
- package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
- package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
- package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
- package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
- package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
- package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
- package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
- package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
- package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
- package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
- package/src/features/automations/action-executors/index.ts +37 -37
- package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
- package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
- package/src/features/automations/action-executors/webhook.executor.ts +67 -67
- package/src/features/automations/automation-runner.ts +85 -85
- package/src/features/automations/automation-runner.utils.ts +47 -47
- package/src/features/automations/automations.handler.ts +197 -197
- package/src/features/automations/automations.schema.ts +177 -177
- package/src/features/automations/context-resolver.ts +152 -152
- package/src/features/automations/cron-runner.ts +140 -140
- package/src/features/automations/cron-runner.utils.ts +44 -44
- package/src/features/automations/filter-translation.ts +46 -46
- package/src/features/automations/index.ts +16 -16
- package/src/features/automations/template-grammar.ts +245 -245
- package/src/features/automations/var-access-resolver.ts +140 -140
- package/src/features/automations/when-evaluator.ts +87 -87
- package/src/features/automations/when-pushdown.ts +57 -57
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
- package/src/features/backrefs/backrefs.handler.ts +168 -168
- package/src/features/backrefs/d1-backref.repository.ts +84 -84
- package/src/features/backrefs/index.ts +5 -5
- package/src/features/content/constants.ts +16 -16
- package/src/features/content/handlers/bulk.handler.ts +185 -185
- package/src/features/content/handlers/create.ts +129 -129
- package/src/features/content/handlers/delete.ts +62 -62
- package/src/features/content/handlers/facets.ts +49 -49
- package/src/features/content/handlers/get.ts +120 -120
- package/src/features/content/handlers/helpers.test.ts +180 -180
- package/src/features/content/handlers/helpers.ts +93 -93
- package/src/features/content/handlers/list.ts +174 -174
- package/src/features/content/handlers/update.ts +174 -174
- package/src/features/content/index.ts +26 -26
- 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 -195
- package/src/features/draft/draft.middleware.ts +66 -66
- package/src/features/draft/index.ts +5 -5
- package/src/features/email/email.provider.ts +42 -42
- package/src/features/email/email.service.ts +98 -98
- package/src/features/email/email.types.ts +119 -119
- package/src/features/email/index.ts +33 -33
- package/src/features/email/providers/resend.ts +67 -67
- package/src/features/email/providers/smtp.ts +55 -55
- package/src/features/email/templates/automation-mail.ts +19 -19
- package/src/features/email/templates/password-changed.ts +63 -63
- package/src/features/email/templates/password-reset.ts +68 -68
- package/src/features/email/templates/shell.ts +96 -96
- package/src/features/notifications/index.ts +5 -5
- package/src/features/notifications/notifications.handler.ts +105 -105
- package/src/features/password-reset/index.ts +19 -19
- package/src/features/password-reset/request.ts +92 -92
- package/src/features/password-reset/reset.ts +102 -102
- package/src/features/rotate-field/index.ts +5 -5
- package/src/features/rotate-field/rotate-field.handler.ts +132 -132
- package/src/features/rotate-field/rotate-field.schema.ts +17 -17
- package/src/features/schema/schema.handler.ts +131 -131
- package/src/features/seeds/index.ts +5 -5
- package/src/features/seeds/seeds.handler.test.ts +1199 -632
- package/src/features/seeds/seeds.handler.ts +558 -693
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
- package/src/features/settings/settings.handler.ts +392 -392
- package/src/features/setup/index.ts +288 -204
- package/src/features/stats/index.ts +5 -5
- package/src/features/stats/stats.handler.ts +420 -420
- package/src/features/webhooks/index.ts +63 -63
- package/src/index.ts +65 -65
- package/src/media-utils.ts +82 -82
- package/src/middleware/auth-providers.middleware.test.ts +85 -0
- package/src/middleware/auth-providers.middleware.ts +45 -45
- package/src/middleware/observability.middleware.test.ts +93 -0
- package/src/middleware/observability.middleware.ts +70 -70
- package/src/middleware/rate-limit.middleware.ts +45 -45
- package/src/middleware/repository.middleware.ts +117 -114
- package/src/middleware/seed-registry.middleware.test.ts +97 -97
- package/src/middleware/seed-registry.middleware.ts +21 -21
- package/src/middleware/storage.middleware.ts +25 -25
- package/src/middleware.ts +45 -45
- package/src/public/access-policy.ts +27 -27
- package/src/public/api-key-middleware.ts +57 -57
- package/src/public/cache-utils.ts +38 -38
- package/src/public/entry-projection.ts +46 -46
- package/src/public/idempotency.ts +23 -23
- package/src/public/index.ts +16 -16
- package/src/public/problem-details.ts +124 -124
- package/src/public/public-add.ts +114 -114
- package/src/public/public-edit.ts +163 -163
- package/src/public/public-errors.ts +19 -19
- package/src/public/public-read.ts +63 -63
- package/src/public/public-routes.ts +88 -88
- package/src/public/query-builder.test.ts +224 -224
- package/src/public/query-builder.ts +156 -156
- package/src/public/rate-limit-middleware.ts +34 -34
- package/src/public/read-list.ts +54 -54
- package/src/public/read-single.ts +48 -48
- package/src/public/response-builder.ts +30 -30
- package/src/public/sanitize.ts +69 -69
- package/src/public/slug-utils.ts +18 -18
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
- package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
- package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
- package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
- package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
- package/src/rate-limit/no-op-rate-limiter.ts +11 -11
- package/src/search-utils.test.ts +211 -211
- package/src/search-utils.ts +213 -213
- package/src/search.ts +65 -65
- package/src/shared/apply-policies.test.ts +81 -81
- package/src/shared/apply-policies.ts +67 -67
- package/src/shared/automations.repository.d1.ts +150 -150
- package/src/shared/background-notification-service.test.ts +62 -62
- package/src/shared/background-notification-service.ts +52 -52
- package/src/shared/base.repository.d1.ts +32 -32
- package/src/shared/content-utils.test.ts +165 -165
- package/src/shared/content-utils.ts +86 -86
- package/src/shared/content.repository.d1.test.ts +507 -458
- package/src/shared/content.repository.d1.ts +851 -851
- package/src/shared/d1-activity-log.repository.test.ts +140 -140
- package/src/shared/d1-activity-log.repository.ts +105 -105
- package/src/shared/d1-activity-logger.test.ts +86 -86
- package/src/shared/d1-activity-logger.ts +67 -67
- package/src/shared/d1-analytics.repository.test.ts +78 -78
- package/src/shared/d1-analytics.repository.ts +85 -85
- package/src/shared/d1-content-scan.repository.test.ts +80 -80
- package/src/shared/d1-content-scan.repository.ts +37 -37
- package/src/shared/d1-notification.repository.test.ts +128 -128
- package/src/shared/d1-notification.repository.ts +118 -118
- package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
- package/src/shared/d1-password-reset-token.repository.ts +56 -56
- package/src/shared/d1-search.repository.test.ts +87 -87
- package/src/shared/d1-search.repository.ts +88 -88
- package/src/shared/d1-session.repository.test.ts +125 -125
- package/src/shared/d1-session.repository.ts +102 -102
- package/src/shared/d1-setup-checklist.repository.ts +36 -36
- package/src/shared/d1-user.repository.test.ts +151 -151
- package/src/shared/d1-user.repository.ts +119 -119
- package/src/shared/d1-widget.repository.test.ts +267 -221
- package/src/shared/d1-widget.repository.ts +385 -341
- 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 -54
- package/src/shared/demo-data.repository.d1.ts +20 -20
- package/src/shared/execution-context-scheduler.ts +13 -13
- package/src/shared/fixed-clock.ts +25 -25
- package/src/shared/fts-sync.ts +8 -8
- package/src/shared/idempotency.repository.d1.test.ts +83 -83
- package/src/shared/idempotency.repository.d1.ts +60 -60
- package/src/shared/in-memory-activity-logger.ts +19 -19
- package/src/shared/in-memory-notification-service.ts +19 -19
- package/src/shared/in-memory-seed.repository.ts +58 -51
- package/src/shared/media.repository.d1.test.ts +107 -107
- package/src/shared/media.repository.d1.ts +68 -68
- package/src/shared/qstash-notification-service.test.ts +67 -67
- package/src/shared/qstash-notification-service.ts +55 -55
- package/src/shared/query-utils.ts +141 -141
- package/src/shared/request-utils.ts +26 -26
- package/src/shared/schema-mutator.d1.test.ts +142 -0
- package/src/shared/schema-mutator.d1.ts +64 -64
- package/src/shared/seed-layout.repository.d1.test.ts +114 -114
- package/src/shared/seed-layout.repository.d1.ts +62 -62
- package/src/shared/seed-registry-cache.test.ts +68 -68
- package/src/shared/seed-registry-cache.ts +49 -49
- package/src/shared/seed.repository.d1.test.ts +143 -143
- package/src/shared/seed.repository.d1.ts +98 -98
- package/src/shared/sequential-id-generator.ts +33 -33
- package/src/shared/site-settings.repository.d1.ts +53 -53
- package/src/shared/storage/factory.ts +41 -41
- package/src/shared/storage/s3-bucket.ts +195 -195
- package/src/shared/storage-utils.ts +40 -40
- package/src/shared/system-stats.repository.d1.test.ts +58 -58
- package/src/shared/system-stats.repository.d1.ts +48 -48
- package/src/types.ts +90 -87
- package/src/upload.ts +198 -198
- package/src/widget.test.ts +85 -0
- package/src/widget.ts +251 -212
- package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
- package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
|
@@ -1,81 +1,81 @@
|
|
|
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 } from 'vitest'
|
|
6
|
-
import { applyPrivacy, applyVisibility, PrivacyPolicyError } from './apply-policies'
|
|
7
|
-
import type { Seed } from '@beechcms/core'
|
|
8
|
-
|
|
9
|
-
function makeSeed(branches: any[]): Seed {
|
|
10
|
-
return { slug: 'test', displayNameAlias: 'title', branches } as unknown as Seed
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// ─── applyPrivacy ─────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
describe('applyPrivacy', () => {
|
|
16
|
-
it('passes through fields with no privacy policy (default: public)', async () => {
|
|
17
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'title', type: 'text' }])
|
|
18
|
-
const result = await applyPrivacy({ title: 'Hello' }, seed)
|
|
19
|
-
expect(result.title).toBe('Hello')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('hashes the value when branch privacy is "hash"', async () => {
|
|
23
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'email', type: 'text', policies: { privacy: 'hash' } }])
|
|
24
|
-
const result = await applyPrivacy({ email: 'user@test.com' }, seed)
|
|
25
|
-
// sha256 produces a 64-char hex string
|
|
26
|
-
expect(typeof result.email).toBe('string')
|
|
27
|
-
expect((result.email as string).length).toBe(64)
|
|
28
|
-
expect(result.email).not.toBe('user@test.com')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('leaves null/undefined values unhashed even when privacy is "hash"', async () => {
|
|
32
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'email', type: 'text', policies: { privacy: 'hash' } }])
|
|
33
|
-
const result = await applyPrivacy({ email: null }, seed)
|
|
34
|
-
expect(result.email).toBeNull()
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('throws PrivacyPolicyError for "encrypt" privacy (not yet implemented)', async () => {
|
|
38
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'secret', type: 'text', policies: { privacy: 'encrypt' } }])
|
|
39
|
-
await expect(applyPrivacy({ secret: 'value' }, seed)).rejects.toBeInstanceOf(PrivacyPolicyError)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('passes through fields not present in the seed branches unchanged', async () => {
|
|
43
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'title', type: 'text' }])
|
|
44
|
-
const result = await applyPrivacy({ title: 'Hi', extraField: 'extra' }, seed)
|
|
45
|
-
expect(result.extraField).toBe('extra')
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
// ─── applyVisibility ──────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
describe('applyVisibility', () => {
|
|
52
|
-
it('includes fields with default (visible) visibility', () => {
|
|
53
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'title', type: 'text' }])
|
|
54
|
-
const result = applyVisibility({ title: 'Hello' }, seed)
|
|
55
|
-
expect(result.title).toBe('Hello')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('omits fields with visibility "hidden"', () => {
|
|
59
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'internal', type: 'text', policies: { visibility: 'hidden' } }])
|
|
60
|
-
const result = applyVisibility({ internal: 'secret' }, seed)
|
|
61
|
-
expect(result).not.toHaveProperty('internal')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('masks non-empty string fields with visibility "masked"', () => {
|
|
65
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'password', type: 'text', policies: { visibility: 'masked' } }])
|
|
66
|
-
const result = applyVisibility({ password: 'secret123' }, seed)
|
|
67
|
-
expect(result.password).toBe('••••••••')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('returns null for empty string fields with visibility "masked"', () => {
|
|
71
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'password', type: 'text', policies: { visibility: 'masked' } }])
|
|
72
|
-
const result = applyVisibility({ password: '' }, seed)
|
|
73
|
-
expect(result.password).toBeNull()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('passes through fields not present in the seed branches', () => {
|
|
77
|
-
const seed = makeSeed([{ id: 'br_01', alias: 'title', type: 'text' }])
|
|
78
|
-
const result = applyVisibility({ title: 'Hi', extraField: 'pass' }, seed)
|
|
79
|
-
expect(result.extraField).toBe('pass')
|
|
80
|
-
})
|
|
81
|
-
})
|
|
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 } from 'vitest'
|
|
6
|
+
import { applyPrivacy, applyVisibility, PrivacyPolicyError } from './apply-policies'
|
|
7
|
+
import type { Seed } from '@beechcms/core'
|
|
8
|
+
|
|
9
|
+
function makeSeed(branches: any[]): Seed {
|
|
10
|
+
return { slug: 'test', displayNameAlias: 'title', branches } as unknown as Seed
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ─── applyPrivacy ─────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
describe('applyPrivacy', () => {
|
|
16
|
+
it('passes through fields with no privacy policy (default: public)', async () => {
|
|
17
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'title', type: 'text' }])
|
|
18
|
+
const result = await applyPrivacy({ title: 'Hello' }, seed)
|
|
19
|
+
expect(result.title).toBe('Hello')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('hashes the value when branch privacy is "hash"', async () => {
|
|
23
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'email', type: 'text', policies: { privacy: 'hash' } }])
|
|
24
|
+
const result = await applyPrivacy({ email: 'user@test.com' }, seed)
|
|
25
|
+
// sha256 produces a 64-char hex string
|
|
26
|
+
expect(typeof result.email).toBe('string')
|
|
27
|
+
expect((result.email as string).length).toBe(64)
|
|
28
|
+
expect(result.email).not.toBe('user@test.com')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('leaves null/undefined values unhashed even when privacy is "hash"', async () => {
|
|
32
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'email', type: 'text', policies: { privacy: 'hash' } }])
|
|
33
|
+
const result = await applyPrivacy({ email: null }, seed)
|
|
34
|
+
expect(result.email).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('throws PrivacyPolicyError for "encrypt" privacy (not yet implemented)', async () => {
|
|
38
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'secret', type: 'text', policies: { privacy: 'encrypt' } }])
|
|
39
|
+
await expect(applyPrivacy({ secret: 'value' }, seed)).rejects.toBeInstanceOf(PrivacyPolicyError)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('passes through fields not present in the seed branches unchanged', async () => {
|
|
43
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'title', type: 'text' }])
|
|
44
|
+
const result = await applyPrivacy({ title: 'Hi', extraField: 'extra' }, seed)
|
|
45
|
+
expect(result.extraField).toBe('extra')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// ─── applyVisibility ──────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe('applyVisibility', () => {
|
|
52
|
+
it('includes fields with default (visible) visibility', () => {
|
|
53
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'title', type: 'text' }])
|
|
54
|
+
const result = applyVisibility({ title: 'Hello' }, seed)
|
|
55
|
+
expect(result.title).toBe('Hello')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('omits fields with visibility "hidden"', () => {
|
|
59
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'internal', type: 'text', policies: { visibility: 'hidden' } }])
|
|
60
|
+
const result = applyVisibility({ internal: 'secret' }, seed)
|
|
61
|
+
expect(result).not.toHaveProperty('internal')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('masks non-empty string fields with visibility "masked"', () => {
|
|
65
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'password', type: 'text', policies: { visibility: 'masked' } }])
|
|
66
|
+
const result = applyVisibility({ password: 'secret123' }, seed)
|
|
67
|
+
expect(result.password).toBe('••••••••')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('returns null for empty string fields with visibility "masked"', () => {
|
|
71
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'password', type: 'text', policies: { visibility: 'masked' } }])
|
|
72
|
+
const result = applyVisibility({ password: '' }, seed)
|
|
73
|
+
expect(result.password).toBeNull()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('passes through fields not present in the seed branches', () => {
|
|
77
|
+
const seed = makeSeed([{ id: 'br_01', alias: 'title', type: 'text' }])
|
|
78
|
+
const result = applyVisibility({ title: 'Hi', extraField: 'pass' }, seed)
|
|
79
|
+
expect(result.extraField).toBe('pass')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -1,67 +1,67 @@
|
|
|
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
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
-
import { resolvePolicies, sha256hex } from '@beechcms/core'
|
|
7
|
-
import type { Seed } from '@beechcms/core'
|
|
8
|
-
|
|
9
|
-
class PrivacyPolicyError extends Error {
|
|
10
|
-
readonly status = 501 as const
|
|
11
|
-
constructor(message: string) {
|
|
12
|
-
super(message)
|
|
13
|
-
this.name = 'PrivacyPolicyError'
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export { PrivacyPolicyError }
|
|
18
|
-
|
|
19
|
-
/** Applica la privacy policy ai campi del payload prima della scrittura su DB. */
|
|
20
|
-
export async function applyPrivacy(
|
|
21
|
-
data: Record<string, unknown>,
|
|
22
|
-
seed: Seed,
|
|
23
|
-
): Promise<Record<string, unknown>> {
|
|
24
|
-
const result: Record<string, unknown> = {}
|
|
25
|
-
for (const [alias, value] of Object.entries(data)) {
|
|
26
|
-
const branch = seed.branches.find((b) => b.alias === alias)
|
|
27
|
-
if (!branch) {
|
|
28
|
-
result[alias] = value
|
|
29
|
-
continue
|
|
30
|
-
}
|
|
31
|
-
const { privacy } = resolvePolicies(branch)
|
|
32
|
-
if (privacy === 'encrypt') {
|
|
33
|
-
throw new PrivacyPolicyError(
|
|
34
|
-
`Field '${alias}' uses 'encrypt' privacy which is not yet implemented.`,
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
if (privacy === 'hash' && value != null) {
|
|
38
|
-
result[alias] = await sha256hex(String(value))
|
|
39
|
-
} else {
|
|
40
|
-
result[alias] = value
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return result
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Applica la visibility policy ai campi del payload in uscita verso il client. */
|
|
47
|
-
export function applyVisibility(
|
|
48
|
-
data: Record<string, unknown>,
|
|
49
|
-
seed: Seed,
|
|
50
|
-
): Record<string, unknown> {
|
|
51
|
-
const result: Record<string, unknown> = {}
|
|
52
|
-
for (const [alias, value] of Object.entries(data)) {
|
|
53
|
-
const branch = seed.branches.find((b) => b.alias === alias)
|
|
54
|
-
if (!branch) {
|
|
55
|
-
result[alias] = value
|
|
56
|
-
continue
|
|
57
|
-
}
|
|
58
|
-
const { visibility } = resolvePolicies(branch)
|
|
59
|
-
if (visibility === 'hidden') continue
|
|
60
|
-
if (visibility === 'masked') {
|
|
61
|
-
result[alias] = typeof value === 'string' && value.length > 0 ? '••••••••' : null
|
|
62
|
-
} else {
|
|
63
|
-
result[alias] = value
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return result
|
|
67
|
-
}
|
|
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
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
+
import { resolvePolicies, sha256hex } from '@beechcms/core'
|
|
7
|
+
import type { Seed } from '@beechcms/core'
|
|
8
|
+
|
|
9
|
+
class PrivacyPolicyError extends Error {
|
|
10
|
+
readonly status = 501 as const
|
|
11
|
+
constructor(message: string) {
|
|
12
|
+
super(message)
|
|
13
|
+
this.name = 'PrivacyPolicyError'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { PrivacyPolicyError }
|
|
18
|
+
|
|
19
|
+
/** Applica la privacy policy ai campi del payload prima della scrittura su DB. */
|
|
20
|
+
export async function applyPrivacy(
|
|
21
|
+
data: Record<string, unknown>,
|
|
22
|
+
seed: Seed,
|
|
23
|
+
): Promise<Record<string, unknown>> {
|
|
24
|
+
const result: Record<string, unknown> = {}
|
|
25
|
+
for (const [alias, value] of Object.entries(data)) {
|
|
26
|
+
const branch = seed.branches.find((b) => b.alias === alias)
|
|
27
|
+
if (!branch) {
|
|
28
|
+
result[alias] = value
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
const { privacy } = resolvePolicies(branch)
|
|
32
|
+
if (privacy === 'encrypt') {
|
|
33
|
+
throw new PrivacyPolicyError(
|
|
34
|
+
`Field '${alias}' uses 'encrypt' privacy which is not yet implemented.`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
if (privacy === 'hash' && value != null) {
|
|
38
|
+
result[alias] = await sha256hex(String(value))
|
|
39
|
+
} else {
|
|
40
|
+
result[alias] = value
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Applica la visibility policy ai campi del payload in uscita verso il client. */
|
|
47
|
+
export function applyVisibility(
|
|
48
|
+
data: Record<string, unknown>,
|
|
49
|
+
seed: Seed,
|
|
50
|
+
): Record<string, unknown> {
|
|
51
|
+
const result: Record<string, unknown> = {}
|
|
52
|
+
for (const [alias, value] of Object.entries(data)) {
|
|
53
|
+
const branch = seed.branches.find((b) => b.alias === alias)
|
|
54
|
+
if (!branch) {
|
|
55
|
+
result[alias] = value
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
const { visibility } = resolvePolicies(branch)
|
|
59
|
+
if (visibility === 'hidden') continue
|
|
60
|
+
if (visibility === 'masked') {
|
|
61
|
+
result[alias] = typeof value === 'string' && value.length > 0 ? '••••••••' : null
|
|
62
|
+
} else {
|
|
63
|
+
result[alias] = value
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
@@ -1,150 +1,150 @@
|
|
|
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 {
|
|
6
|
-
IAutomationRepository,
|
|
7
|
-
Automation,
|
|
8
|
-
AutomationAction,
|
|
9
|
-
AutomationTrigger,
|
|
10
|
-
AutomationTriggerEvent,
|
|
11
|
-
CreateAutomationInput,
|
|
12
|
-
UpdateAutomationInput,
|
|
13
|
-
WhenNode,
|
|
14
|
-
} from '@beechcms/core'
|
|
15
|
-
|
|
16
|
-
interface AutomationRow {
|
|
17
|
-
id: string
|
|
18
|
-
seed_slug: string
|
|
19
|
-
name: string
|
|
20
|
-
enabled: number
|
|
21
|
-
triggers: string
|
|
22
|
-
trigger_conditions: string | null
|
|
23
|
-
actions: string
|
|
24
|
-
created_at: number
|
|
25
|
-
updated_at: number
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class D1AutomationRepository implements IAutomationRepository {
|
|
29
|
-
constructor(private readonly db: D1Database) {}
|
|
30
|
-
|
|
31
|
-
async findActive(seedSlug: string, event: AutomationTriggerEvent): Promise<Automation[]> {
|
|
32
|
-
const result = seedSlug === '*'
|
|
33
|
-
? await this.db
|
|
34
|
-
.prepare(
|
|
35
|
-
`SELECT DISTINCT a.* FROM automations a, json_each(a.triggers) t
|
|
36
|
-
WHERE a.enabled = 1 AND json_extract(t.value, '$.event') = ?`,
|
|
37
|
-
)
|
|
38
|
-
.bind(event)
|
|
39
|
-
.all<AutomationRow>()
|
|
40
|
-
: await this.db
|
|
41
|
-
.prepare(
|
|
42
|
-
`SELECT DISTINCT a.* FROM automations a, json_each(a.triggers) t
|
|
43
|
-
WHERE a.seed_slug = ? AND a.enabled = 1 AND json_extract(t.value, '$.event') = ?`,
|
|
44
|
-
)
|
|
45
|
-
.bind(seedSlug, event)
|
|
46
|
-
.all<AutomationRow>()
|
|
47
|
-
return (result.results ?? []).map(rowToAutomation)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async list(seedSlug: string): Promise<Automation[]> {
|
|
51
|
-
const result = await this.db
|
|
52
|
-
.prepare(`SELECT * FROM automations WHERE seed_slug = ? ORDER BY created_at DESC`)
|
|
53
|
-
.bind(seedSlug)
|
|
54
|
-
.all<AutomationRow>()
|
|
55
|
-
return (result.results ?? []).map(rowToAutomation)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async findById(id: string): Promise<Automation | null> {
|
|
59
|
-
const row = await this.db
|
|
60
|
-
.prepare(`SELECT * FROM automations WHERE id = ?`)
|
|
61
|
-
.bind(id)
|
|
62
|
-
.first<AutomationRow>()
|
|
63
|
-
return row ? rowToAutomation(row) : null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async create(input: CreateAutomationInput): Promise<string> {
|
|
67
|
-
const id = crypto.randomUUID()
|
|
68
|
-
const now = Math.floor(Date.now() / 1000)
|
|
69
|
-
await this.db
|
|
70
|
-
.prepare(
|
|
71
|
-
`INSERT INTO automations
|
|
72
|
-
(id, seed_slug, name, enabled, triggers, trigger_conditions, actions, created_at, updated_at)
|
|
73
|
-
VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?)`,
|
|
74
|
-
)
|
|
75
|
-
.bind(
|
|
76
|
-
id,
|
|
77
|
-
input.seed_slug,
|
|
78
|
-
input.name,
|
|
79
|
-
JSON.stringify(input.triggers),
|
|
80
|
-
input.trigger_conditions ? JSON.stringify(input.trigger_conditions) : null,
|
|
81
|
-
JSON.stringify(input.actions),
|
|
82
|
-
now,
|
|
83
|
-
now,
|
|
84
|
-
)
|
|
85
|
-
.run()
|
|
86
|
-
return id
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async update(id: string, input: UpdateAutomationInput): Promise<void> {
|
|
90
|
-
const now = Math.floor(Date.now() / 1000)
|
|
91
|
-
const fields: string[] = []
|
|
92
|
-
const values: unknown[] = []
|
|
93
|
-
|
|
94
|
-
const map: Record<string, unknown> = {
|
|
95
|
-
seed_slug: input.seed_slug,
|
|
96
|
-
name: input.name,
|
|
97
|
-
triggers: input.triggers !== undefined ? JSON.stringify(input.triggers) : undefined,
|
|
98
|
-
trigger_conditions:
|
|
99
|
-
input.trigger_conditions !== undefined
|
|
100
|
-
? input.trigger_conditions === null
|
|
101
|
-
? null
|
|
102
|
-
: JSON.stringify(input.trigger_conditions)
|
|
103
|
-
: undefined,
|
|
104
|
-
actions: input.actions !== undefined ? JSON.stringify(input.actions) : undefined,
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const [column, value] of Object.entries(map)) {
|
|
108
|
-
if (value !== undefined) {
|
|
109
|
-
fields.push(`${column} = ?`)
|
|
110
|
-
values.push(value)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (fields.length === 0) return
|
|
115
|
-
|
|
116
|
-
values.push(now, id)
|
|
117
|
-
await this.db
|
|
118
|
-
.prepare(`UPDATE automations SET ${fields.join(', ')}, updated_at = ? WHERE id = ?`)
|
|
119
|
-
.bind(...values)
|
|
120
|
-
.run()
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async toggle(id: string, enabled: boolean): Promise<void> {
|
|
124
|
-
const now = Math.floor(Date.now() / 1000)
|
|
125
|
-
await this.db
|
|
126
|
-
.prepare(`UPDATE automations SET enabled = ?, updated_at = ? WHERE id = ?`)
|
|
127
|
-
.bind(enabled ? 1 : 0, now, id)
|
|
128
|
-
.run()
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async delete(id: string): Promise<void> {
|
|
132
|
-
await this.db.prepare(`DELETE FROM automations WHERE id = ?`).bind(id).run()
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function rowToAutomation(row: AutomationRow): Automation {
|
|
137
|
-
return {
|
|
138
|
-
id: row.id,
|
|
139
|
-
seed_slug: row.seed_slug,
|
|
140
|
-
name: row.name,
|
|
141
|
-
enabled: row.enabled === 1,
|
|
142
|
-
triggers: JSON.parse(row.triggers) as AutomationTrigger[],
|
|
143
|
-
trigger_conditions: row.trigger_conditions
|
|
144
|
-
? (JSON.parse(row.trigger_conditions) as WhenNode)
|
|
145
|
-
: null,
|
|
146
|
-
actions: JSON.parse(row.actions) as AutomationAction[],
|
|
147
|
-
created_at: row.created_at,
|
|
148
|
-
updated_at: row.updated_at,
|
|
149
|
-
}
|
|
150
|
-
}
|
|
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 {
|
|
6
|
+
IAutomationRepository,
|
|
7
|
+
Automation,
|
|
8
|
+
AutomationAction,
|
|
9
|
+
AutomationTrigger,
|
|
10
|
+
AutomationTriggerEvent,
|
|
11
|
+
CreateAutomationInput,
|
|
12
|
+
UpdateAutomationInput,
|
|
13
|
+
WhenNode,
|
|
14
|
+
} from '@beechcms/core'
|
|
15
|
+
|
|
16
|
+
interface AutomationRow {
|
|
17
|
+
id: string
|
|
18
|
+
seed_slug: string
|
|
19
|
+
name: string
|
|
20
|
+
enabled: number
|
|
21
|
+
triggers: string
|
|
22
|
+
trigger_conditions: string | null
|
|
23
|
+
actions: string
|
|
24
|
+
created_at: number
|
|
25
|
+
updated_at: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class D1AutomationRepository implements IAutomationRepository {
|
|
29
|
+
constructor(private readonly db: D1Database) {}
|
|
30
|
+
|
|
31
|
+
async findActive(seedSlug: string, event: AutomationTriggerEvent): Promise<Automation[]> {
|
|
32
|
+
const result = seedSlug === '*'
|
|
33
|
+
? await this.db
|
|
34
|
+
.prepare(
|
|
35
|
+
`SELECT DISTINCT a.* FROM automations a, json_each(a.triggers) t
|
|
36
|
+
WHERE a.enabled = 1 AND json_extract(t.value, '$.event') = ?`,
|
|
37
|
+
)
|
|
38
|
+
.bind(event)
|
|
39
|
+
.all<AutomationRow>()
|
|
40
|
+
: await this.db
|
|
41
|
+
.prepare(
|
|
42
|
+
`SELECT DISTINCT a.* FROM automations a, json_each(a.triggers) t
|
|
43
|
+
WHERE a.seed_slug = ? AND a.enabled = 1 AND json_extract(t.value, '$.event') = ?`,
|
|
44
|
+
)
|
|
45
|
+
.bind(seedSlug, event)
|
|
46
|
+
.all<AutomationRow>()
|
|
47
|
+
return (result.results ?? []).map(rowToAutomation)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async list(seedSlug: string): Promise<Automation[]> {
|
|
51
|
+
const result = await this.db
|
|
52
|
+
.prepare(`SELECT * FROM automations WHERE seed_slug = ? ORDER BY created_at DESC`)
|
|
53
|
+
.bind(seedSlug)
|
|
54
|
+
.all<AutomationRow>()
|
|
55
|
+
return (result.results ?? []).map(rowToAutomation)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async findById(id: string): Promise<Automation | null> {
|
|
59
|
+
const row = await this.db
|
|
60
|
+
.prepare(`SELECT * FROM automations WHERE id = ?`)
|
|
61
|
+
.bind(id)
|
|
62
|
+
.first<AutomationRow>()
|
|
63
|
+
return row ? rowToAutomation(row) : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async create(input: CreateAutomationInput): Promise<string> {
|
|
67
|
+
const id = crypto.randomUUID()
|
|
68
|
+
const now = Math.floor(Date.now() / 1000)
|
|
69
|
+
await this.db
|
|
70
|
+
.prepare(
|
|
71
|
+
`INSERT INTO automations
|
|
72
|
+
(id, seed_slug, name, enabled, triggers, trigger_conditions, actions, created_at, updated_at)
|
|
73
|
+
VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?)`,
|
|
74
|
+
)
|
|
75
|
+
.bind(
|
|
76
|
+
id,
|
|
77
|
+
input.seed_slug,
|
|
78
|
+
input.name,
|
|
79
|
+
JSON.stringify(input.triggers),
|
|
80
|
+
input.trigger_conditions ? JSON.stringify(input.trigger_conditions) : null,
|
|
81
|
+
JSON.stringify(input.actions),
|
|
82
|
+
now,
|
|
83
|
+
now,
|
|
84
|
+
)
|
|
85
|
+
.run()
|
|
86
|
+
return id
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async update(id: string, input: UpdateAutomationInput): Promise<void> {
|
|
90
|
+
const now = Math.floor(Date.now() / 1000)
|
|
91
|
+
const fields: string[] = []
|
|
92
|
+
const values: unknown[] = []
|
|
93
|
+
|
|
94
|
+
const map: Record<string, unknown> = {
|
|
95
|
+
seed_slug: input.seed_slug,
|
|
96
|
+
name: input.name,
|
|
97
|
+
triggers: input.triggers !== undefined ? JSON.stringify(input.triggers) : undefined,
|
|
98
|
+
trigger_conditions:
|
|
99
|
+
input.trigger_conditions !== undefined
|
|
100
|
+
? input.trigger_conditions === null
|
|
101
|
+
? null
|
|
102
|
+
: JSON.stringify(input.trigger_conditions)
|
|
103
|
+
: undefined,
|
|
104
|
+
actions: input.actions !== undefined ? JSON.stringify(input.actions) : undefined,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const [column, value] of Object.entries(map)) {
|
|
108
|
+
if (value !== undefined) {
|
|
109
|
+
fields.push(`${column} = ?`)
|
|
110
|
+
values.push(value)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (fields.length === 0) return
|
|
115
|
+
|
|
116
|
+
values.push(now, id)
|
|
117
|
+
await this.db
|
|
118
|
+
.prepare(`UPDATE automations SET ${fields.join(', ')}, updated_at = ? WHERE id = ?`)
|
|
119
|
+
.bind(...values)
|
|
120
|
+
.run()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async toggle(id: string, enabled: boolean): Promise<void> {
|
|
124
|
+
const now = Math.floor(Date.now() / 1000)
|
|
125
|
+
await this.db
|
|
126
|
+
.prepare(`UPDATE automations SET enabled = ?, updated_at = ? WHERE id = ?`)
|
|
127
|
+
.bind(enabled ? 1 : 0, now, id)
|
|
128
|
+
.run()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async delete(id: string): Promise<void> {
|
|
132
|
+
await this.db.prepare(`DELETE FROM automations WHERE id = ?`).bind(id).run()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function rowToAutomation(row: AutomationRow): Automation {
|
|
137
|
+
return {
|
|
138
|
+
id: row.id,
|
|
139
|
+
seed_slug: row.seed_slug,
|
|
140
|
+
name: row.name,
|
|
141
|
+
enabled: row.enabled === 1,
|
|
142
|
+
triggers: JSON.parse(row.triggers) as AutomationTrigger[],
|
|
143
|
+
trigger_conditions: row.trigger_conditions
|
|
144
|
+
? (JSON.parse(row.trigger_conditions) as WhenNode)
|
|
145
|
+
: null,
|
|
146
|
+
actions: JSON.parse(row.actions) as AutomationAction[],
|
|
147
|
+
created_at: row.created_at,
|
|
148
|
+
updated_at: row.updated_at,
|
|
149
|
+
}
|
|
150
|
+
}
|