@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,174 +1,174 @@
|
|
|
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 { Context } from 'hono'
|
|
6
|
-
import {
|
|
7
|
-
slugify,
|
|
8
|
-
isValidContentStatus,
|
|
9
|
-
validateAndSanitizeSeedPayload,
|
|
10
|
-
resolvePolicies
|
|
11
|
-
} from '@beechcms/core'
|
|
12
|
-
import { applyPrivacy, PrivacyPolicyError } from '../../../shared/apply-policies'
|
|
13
|
-
import { publicProblem } from '../../../public/problem-details'
|
|
14
|
-
import {
|
|
15
|
-
normalizeBody,
|
|
16
|
-
contentValidationProblem,
|
|
17
|
-
logContentActivity,
|
|
18
|
-
dispatchContentAutomation,
|
|
19
|
-
handleContentDatabaseError
|
|
20
|
-
} from './helpers'
|
|
21
|
-
import { CONTENT_ERRORS } from '../constants'
|
|
22
|
-
import { cleanStr } from '../../../shared/query-utils'
|
|
23
|
-
import { AppEnv } from '../../../types'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export async function updateHandler(context: Context<AppEnv>) {
|
|
28
|
-
const slug = context.req.param('slug')
|
|
29
|
-
const id = context.req.param('id')
|
|
30
|
-
if (!slug || !id) {
|
|
31
|
-
return publicProblem(context, {
|
|
32
|
-
type: 'content-invalid-slug-or-id',
|
|
33
|
-
title: 'Bad Request',
|
|
34
|
-
status: 400,
|
|
35
|
-
detail: CONTENT_ERRORS.INVALID_SLUG_OR_ID
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const seed = context.get('getSeed')(slug)
|
|
40
|
-
if (!seed) {
|
|
41
|
-
return publicProblem(context, {
|
|
42
|
-
type: 'content-seed-not-found',
|
|
43
|
-
title: 'Not Found',
|
|
44
|
-
status: 404,
|
|
45
|
-
detail: CONTENT_ERRORS.SEED_NOT_FOUND
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let body: Record<string, unknown>
|
|
50
|
-
try {
|
|
51
|
-
body = normalizeBody(await context.req.json<unknown>())
|
|
52
|
-
} catch {
|
|
53
|
-
return publicProblem(context, {
|
|
54
|
-
type: 'content-invalid-json',
|
|
55
|
-
title: 'Bad Request',
|
|
56
|
-
status: 400,
|
|
57
|
-
detail: CONTENT_ERRORS.INVALID_JSON_BODY
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const bodyForData = { ...body }
|
|
62
|
-
delete bodyForData.slug
|
|
63
|
-
delete bodyForData.status
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const repository = context.get('repository')
|
|
67
|
-
// We need current data to handle slug and status if not provided,
|
|
68
|
-
// and to verify existence.
|
|
69
|
-
const current = await repository.findById(seed, id)
|
|
70
|
-
|
|
71
|
-
const newSlug = body.slug !== undefined ? slugify(String(body.slug)) : (current.slug as string)
|
|
72
|
-
const newStatus = body.status !== undefined ? (cleanStr(body.status) ?? current.status as string) : current.status as string
|
|
73
|
-
|
|
74
|
-
if (!isValidContentStatus(newStatus)) {
|
|
75
|
-
return publicProblem(context, {
|
|
76
|
-
type: 'content-invalid-status',
|
|
77
|
-
title: 'Bad Request',
|
|
78
|
-
status: 400,
|
|
79
|
-
detail: 'Invalid status. Allowed values are: draft, review, published'
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!newSlug) {
|
|
84
|
-
return publicProblem(context, {
|
|
85
|
-
type: 'content-missing-slug',
|
|
86
|
-
title: 'Bad Request',
|
|
87
|
-
status: 400,
|
|
88
|
-
detail: 'Missing required field: slug'
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
let mergedData: Record<string, unknown> = {}
|
|
93
|
-
|
|
94
|
-
if (Object.keys(bodyForData).length > 0) {
|
|
95
|
-
const sensitiveAliases = Object.keys(bodyForData).filter((alias) => {
|
|
96
|
-
const branch = seed.branches.find((b) => b.alias === alias)
|
|
97
|
-
return branch != null && resolvePolicies(branch).privacy !== 'plain'
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
if (sensitiveAliases.length > 0) {
|
|
101
|
-
return publicProblem(context, {
|
|
102
|
-
type: 'content-sensitive-field-edit',
|
|
103
|
-
title: 'Unprocessable Entity',
|
|
104
|
-
status: 422,
|
|
105
|
-
detail: `${CONTENT_ERRORS.SENSITIVE_FIELD_EDIT}: ${sensitiveAliases.join(', ')}`
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const validation = validateAndSanitizeSeedPayload(seed, bodyForData, {
|
|
110
|
-
operation: 'update',
|
|
111
|
-
allowNull: true,
|
|
112
|
-
requireAtLeastOneValidField: true,
|
|
113
|
-
enforceRequiredFields: true,
|
|
114
|
-
idGenerator: context.get('idGenerator'),
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
if (validation.dangerousFields.length > 0) {
|
|
118
|
-
return publicProblem(context, {
|
|
119
|
-
type: 'content-dangerous-content',
|
|
120
|
-
title: 'Unprocessable Entity',
|
|
121
|
-
status: 422,
|
|
122
|
-
detail: `Content rejected: dangerous markup detected in field '${validation.dangerousFields[0]}'`
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (validation.details.length > 0) return contentValidationProblem(context, validation.details)
|
|
127
|
-
|
|
128
|
-
let privacyPatch: Record<string, unknown>
|
|
129
|
-
try {
|
|
130
|
-
privacyPatch = await applyPrivacy(validation.data, seed)
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (error instanceof PrivacyPolicyError) {
|
|
133
|
-
return publicProblem(context, {
|
|
134
|
-
type: 'content-policy-not-implemented',
|
|
135
|
-
title: 'Not Implemented',
|
|
136
|
-
status: 501,
|
|
137
|
-
detail: error.message
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
throw error
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Remove null values from patch (patch semantics: null = leave unchanged)
|
|
144
|
-
for (const [k, v] of Object.entries(privacyPatch)) {
|
|
145
|
-
if (v !== null) mergedData[k] = v
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (newSlug !== current.slug) {
|
|
150
|
-
if (await repository.existsSlug(seed, newSlug, id)) {
|
|
151
|
-
return publicProblem(context, {
|
|
152
|
-
type: 'content-slug-conflict',
|
|
153
|
-
title: 'Conflict',
|
|
154
|
-
status: 409,
|
|
155
|
-
detail: CONTENT_ERRORS.SLUG_CONFLICT
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
// Add slug to mergedData if changed
|
|
159
|
-
mergedData.slug = newSlug
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
await repository.update(seed, id, mergedData, newStatus)
|
|
163
|
-
|
|
164
|
-
const jwtPayload = context.get('jwtPayload')
|
|
165
|
-
const title = mergedData.title || mergedData.name || newSlug
|
|
166
|
-
|
|
167
|
-
logContentActivity(context, 'update', id, slug, String(title))
|
|
168
|
-
dispatchContentAutomation(context, slug, 'update', { ...current, ...mergedData, id, status: newStatus })
|
|
169
|
-
|
|
170
|
-
return context.json({ success: true })
|
|
171
|
-
} catch (error) {
|
|
172
|
-
return handleContentDatabaseError(context, error)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
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 { Context } from 'hono'
|
|
6
|
+
import {
|
|
7
|
+
slugify,
|
|
8
|
+
isValidContentStatus,
|
|
9
|
+
validateAndSanitizeSeedPayload,
|
|
10
|
+
resolvePolicies
|
|
11
|
+
} from '@beechcms/core'
|
|
12
|
+
import { applyPrivacy, PrivacyPolicyError } from '../../../shared/apply-policies'
|
|
13
|
+
import { publicProblem } from '../../../public/problem-details'
|
|
14
|
+
import {
|
|
15
|
+
normalizeBody,
|
|
16
|
+
contentValidationProblem,
|
|
17
|
+
logContentActivity,
|
|
18
|
+
dispatchContentAutomation,
|
|
19
|
+
handleContentDatabaseError
|
|
20
|
+
} from './helpers'
|
|
21
|
+
import { CONTENT_ERRORS } from '../constants'
|
|
22
|
+
import { cleanStr } from '../../../shared/query-utils'
|
|
23
|
+
import { AppEnv } from '../../../types'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export async function updateHandler(context: Context<AppEnv>) {
|
|
28
|
+
const slug = context.req.param('slug')
|
|
29
|
+
const id = context.req.param('id')
|
|
30
|
+
if (!slug || !id) {
|
|
31
|
+
return publicProblem(context, {
|
|
32
|
+
type: 'content-invalid-slug-or-id',
|
|
33
|
+
title: 'Bad Request',
|
|
34
|
+
status: 400,
|
|
35
|
+
detail: CONTENT_ERRORS.INVALID_SLUG_OR_ID
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const seed = context.get('getSeed')(slug)
|
|
40
|
+
if (!seed) {
|
|
41
|
+
return publicProblem(context, {
|
|
42
|
+
type: 'content-seed-not-found',
|
|
43
|
+
title: 'Not Found',
|
|
44
|
+
status: 404,
|
|
45
|
+
detail: CONTENT_ERRORS.SEED_NOT_FOUND
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let body: Record<string, unknown>
|
|
50
|
+
try {
|
|
51
|
+
body = normalizeBody(await context.req.json<unknown>())
|
|
52
|
+
} catch {
|
|
53
|
+
return publicProblem(context, {
|
|
54
|
+
type: 'content-invalid-json',
|
|
55
|
+
title: 'Bad Request',
|
|
56
|
+
status: 400,
|
|
57
|
+
detail: CONTENT_ERRORS.INVALID_JSON_BODY
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const bodyForData = { ...body }
|
|
62
|
+
delete bodyForData.slug
|
|
63
|
+
delete bodyForData.status
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const repository = context.get('repository')
|
|
67
|
+
// We need current data to handle slug and status if not provided,
|
|
68
|
+
// and to verify existence.
|
|
69
|
+
const current = await repository.findById(seed, id)
|
|
70
|
+
|
|
71
|
+
const newSlug = body.slug !== undefined ? slugify(String(body.slug)) : (current.slug as string)
|
|
72
|
+
const newStatus = body.status !== undefined ? (cleanStr(body.status) ?? current.status as string) : current.status as string
|
|
73
|
+
|
|
74
|
+
if (!isValidContentStatus(newStatus)) {
|
|
75
|
+
return publicProblem(context, {
|
|
76
|
+
type: 'content-invalid-status',
|
|
77
|
+
title: 'Bad Request',
|
|
78
|
+
status: 400,
|
|
79
|
+
detail: 'Invalid status. Allowed values are: draft, review, published'
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!newSlug) {
|
|
84
|
+
return publicProblem(context, {
|
|
85
|
+
type: 'content-missing-slug',
|
|
86
|
+
title: 'Bad Request',
|
|
87
|
+
status: 400,
|
|
88
|
+
detail: 'Missing required field: slug'
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let mergedData: Record<string, unknown> = {}
|
|
93
|
+
|
|
94
|
+
if (Object.keys(bodyForData).length > 0) {
|
|
95
|
+
const sensitiveAliases = Object.keys(bodyForData).filter((alias) => {
|
|
96
|
+
const branch = seed.branches.find((b) => b.alias === alias)
|
|
97
|
+
return branch != null && resolvePolicies(branch).privacy !== 'plain'
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (sensitiveAliases.length > 0) {
|
|
101
|
+
return publicProblem(context, {
|
|
102
|
+
type: 'content-sensitive-field-edit',
|
|
103
|
+
title: 'Unprocessable Entity',
|
|
104
|
+
status: 422,
|
|
105
|
+
detail: `${CONTENT_ERRORS.SENSITIVE_FIELD_EDIT}: ${sensitiveAliases.join(', ')}`
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const validation = validateAndSanitizeSeedPayload(seed, bodyForData, {
|
|
110
|
+
operation: 'update',
|
|
111
|
+
allowNull: true,
|
|
112
|
+
requireAtLeastOneValidField: true,
|
|
113
|
+
enforceRequiredFields: true,
|
|
114
|
+
idGenerator: context.get('idGenerator'),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (validation.dangerousFields.length > 0) {
|
|
118
|
+
return publicProblem(context, {
|
|
119
|
+
type: 'content-dangerous-content',
|
|
120
|
+
title: 'Unprocessable Entity',
|
|
121
|
+
status: 422,
|
|
122
|
+
detail: `Content rejected: dangerous markup detected in field '${validation.dangerousFields[0]}'`
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (validation.details.length > 0) return contentValidationProblem(context, validation.details)
|
|
127
|
+
|
|
128
|
+
let privacyPatch: Record<string, unknown>
|
|
129
|
+
try {
|
|
130
|
+
privacyPatch = await applyPrivacy(validation.data, seed)
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof PrivacyPolicyError) {
|
|
133
|
+
return publicProblem(context, {
|
|
134
|
+
type: 'content-policy-not-implemented',
|
|
135
|
+
title: 'Not Implemented',
|
|
136
|
+
status: 501,
|
|
137
|
+
detail: error.message
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
throw error
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Remove null values from patch (patch semantics: null = leave unchanged)
|
|
144
|
+
for (const [k, v] of Object.entries(privacyPatch)) {
|
|
145
|
+
if (v !== null) mergedData[k] = v
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (newSlug !== current.slug) {
|
|
150
|
+
if (await repository.existsSlug(seed, newSlug, id)) {
|
|
151
|
+
return publicProblem(context, {
|
|
152
|
+
type: 'content-slug-conflict',
|
|
153
|
+
title: 'Conflict',
|
|
154
|
+
status: 409,
|
|
155
|
+
detail: CONTENT_ERRORS.SLUG_CONFLICT
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
// Add slug to mergedData if changed
|
|
159
|
+
mergedData.slug = newSlug
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await repository.update(seed, id, mergedData, newStatus)
|
|
163
|
+
|
|
164
|
+
const jwtPayload = context.get('jwtPayload')
|
|
165
|
+
const title = mergedData.title || mergedData.name || newSlug
|
|
166
|
+
|
|
167
|
+
logContentActivity(context, 'update', id, slug, String(title))
|
|
168
|
+
dispatchContentAutomation(context, slug, 'update', { ...current, ...mergedData, id, status: newStatus })
|
|
169
|
+
|
|
170
|
+
return context.json({ success: true })
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return handleContentDatabaseError(context, error)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -1,26 +1,26 @@
|
|
|
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 { Hono } from 'hono'
|
|
6
|
-
import { AppEnv } from '../../types'
|
|
7
|
-
import { listHandler } from './handlers/list'
|
|
8
|
-
import { getByIdHandler, getBySlugHandler } from './handlers/get'
|
|
9
|
-
import { createHandler } from './handlers/create'
|
|
10
|
-
import { updateHandler } from './handlers/update'
|
|
11
|
-
import { deleteHandler } from './handlers/delete'
|
|
12
|
-
import { facetsHandler } from './handlers/facets'
|
|
13
|
-
import { bulkHandler } from './handlers/bulk.handler'
|
|
14
|
-
|
|
15
|
-
const content = new Hono<AppEnv>()
|
|
16
|
-
|
|
17
|
-
content.get('/:slug', listHandler)
|
|
18
|
-
content.get('/:slug/facets', facetsHandler)
|
|
19
|
-
content.get('/:schema_slug/by-slug/:entry_slug', getBySlugHandler)
|
|
20
|
-
content.get('/:slug/:id', getByIdHandler)
|
|
21
|
-
content.post('/:slug', createHandler)
|
|
22
|
-
content.patch('/:slug/bulk', bulkHandler)
|
|
23
|
-
content.put('/:slug/:id', updateHandler)
|
|
24
|
-
content.delete('/:slug/:id', deleteHandler)
|
|
25
|
-
|
|
26
|
-
export default content
|
|
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 { Hono } from 'hono'
|
|
6
|
+
import { AppEnv } from '../../types'
|
|
7
|
+
import { listHandler } from './handlers/list'
|
|
8
|
+
import { getByIdHandler, getBySlugHandler } from './handlers/get'
|
|
9
|
+
import { createHandler } from './handlers/create'
|
|
10
|
+
import { updateHandler } from './handlers/update'
|
|
11
|
+
import { deleteHandler } from './handlers/delete'
|
|
12
|
+
import { facetsHandler } from './handlers/facets'
|
|
13
|
+
import { bulkHandler } from './handlers/bulk.handler'
|
|
14
|
+
|
|
15
|
+
const content = new Hono<AppEnv>()
|
|
16
|
+
|
|
17
|
+
content.get('/:slug', listHandler)
|
|
18
|
+
content.get('/:slug/facets', facetsHandler)
|
|
19
|
+
content.get('/:schema_slug/by-slug/:entry_slug', getBySlugHandler)
|
|
20
|
+
content.get('/:slug/:id', getByIdHandler)
|
|
21
|
+
content.post('/:slug', createHandler)
|
|
22
|
+
content.patch('/:slug/bulk', bulkHandler)
|
|
23
|
+
content.put('/:slug/:id', updateHandler)
|
|
24
|
+
content.delete('/:slug/:id', deleteHandler)
|
|
25
|
+
|
|
26
|
+
export default content
|