@beechcms/api 0.4.0-preview.12 → 0.4.0-preview.13
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/index-CewtCjom.css +1 -0
- package/assets/dashboard/assets/{index-CTSuGxlX.js → index-FQ6JhvRH.js} +99 -99
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +213 -213
- package/package.json +2 -2
- package/src/auth/constants.ts +10 -10
- package/src/auth/login.ts +91 -91
- package/src/auth/refresh.ts +127 -127
- package/src/factory.ts +347 -325
- package/src/features/content/constants.ts +10 -10
- package/src/features/content/handlers/create.ts +153 -163
- package/src/features/content/handlers/delete.ts +76 -85
- package/src/features/content/handlers/facets.ts +45 -45
- package/src/features/content/handlers/get.ts +116 -116
- package/src/features/content/handlers/list.ts +88 -88
- package/src/features/content/handlers/update.ts +207 -216
- package/src/features/content/index.ts +20 -20
- package/src/features/draft/draft.handler.ts +272 -272
- package/src/features/draft/index.ts +1 -1
- package/src/features/email/email.provider.ts +38 -38
- package/src/features/email/email.service.ts +80 -80
- package/src/features/email/email.types.ts +98 -98
- package/src/features/email/index.ts +28 -28
- package/src/features/email/providers/resend.ts +63 -63
- package/src/features/email/templates/password-changed.ts +59 -59
- package/src/features/email/templates/password-reset.ts +64 -64
- package/src/features/email/templates/shell.ts +92 -93
- package/src/features/notifications/index.ts +1 -1
- package/src/features/notifications/notifications.handler.ts +130 -88
- package/src/features/password-reset/index.ts +15 -15
- package/src/features/password-reset/request.ts +106 -88
- package/src/features/password-reset/reset.ts +128 -110
- package/src/features/rotate-field/index.ts +1 -1
- package/src/features/rotate-field/rotate-field.handler.ts +132 -82
- package/src/features/rotate-field/rotate-field.schema.ts +13 -9
- package/src/features/schema/schema.handler.ts +16 -16
- package/src/features/settings/settings.handler.ts +414 -267
- package/src/features/setup/index.ts +96 -59
- package/src/features/stats/index.ts +1 -1
- package/src/features/stats/stats.handler.ts +458 -395
- package/src/index.ts +24 -24
- package/src/media-utils.ts +78 -78
- package/src/middleware/repository.middleware.ts +24 -18
- package/src/middleware/storage.middleware.ts +21 -0
- package/src/middleware.ts +67 -67
- package/src/public/access-policy.ts +23 -23
- package/src/public/api-key-middleware.ts +53 -53
- package/src/public/index.ts +12 -12
- package/src/public/problem-details.ts +48 -42
- package/src/public/public-add.ts +166 -166
- package/src/public/public-edit.ts +158 -158
- package/src/public/public-errors.ts +15 -15
- package/src/public/public-read.ts +216 -216
- package/src/public/public-routes.ts +84 -31
- package/src/public/query-builder.ts +152 -152
- package/src/public/rate-limit-middleware.ts +42 -42
- package/src/public/response-builder.ts +26 -26
- package/src/public/sanitize.ts +65 -65
- package/src/public/slug-utils.ts +14 -14
- package/src/search-utils.ts +192 -192
- package/src/search.ts +72 -72
- package/src/shared/activity-logger.ts +79 -79
- package/src/shared/apply-policies.ts +63 -63
- package/src/shared/base.repository.d1.ts +28 -28
- package/src/shared/content-utils.ts +82 -108
- package/src/shared/content.repository.d1.ts +382 -382
- package/src/shared/fts-sync.ts +4 -4
- package/src/shared/idempotency.repository.d1.ts +56 -45
- package/src/shared/media.repository.d1.ts +64 -0
- package/src/shared/notification-service.ts +56 -56
- package/src/shared/query-utils.ts +137 -137
- package/src/shared/storage/factory.ts +40 -0
- package/src/shared/storage/r2-binding-bucket.ts +81 -0
- package/src/shared/storage/s3-bucket.ts +163 -0
- package/src/shared/storage-utils.ts +36 -36
- package/src/shared/system-stats.repository.d1.ts +44 -0
- package/src/types.ts +46 -41
- package/src/upload.ts +179 -331
- package/src/widget.ts +349 -349
- package/assets/dashboard/assets/index-ye3325L9.css +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export const CONTENT_ERRORS = {
|
|
2
|
-
INVALID_SLUG: 'Invalid slug',
|
|
3
|
-
INVALID_SLUG_OR_ID: 'Invalid slug or id',
|
|
4
|
-
INVALID_JSON_BODY: 'Invalid JSON body',
|
|
5
|
-
NOT_FOUND: 'Not found',
|
|
6
|
-
SEED_NOT_FOUND: 'Seed not found',
|
|
7
|
-
DATABASE_ERROR: 'Database error',
|
|
8
|
-
SLUG_CONFLICT: 'Slug already exists for this schema',
|
|
9
|
-
SENSITIVE_FIELD_EDIT: 'Cannot edit sensitive fields',
|
|
10
|
-
} as const
|
|
1
|
+
export const CONTENT_ERRORS = {
|
|
2
|
+
INVALID_SLUG: 'Invalid slug',
|
|
3
|
+
INVALID_SLUG_OR_ID: 'Invalid slug or id',
|
|
4
|
+
INVALID_JSON_BODY: 'Invalid JSON body',
|
|
5
|
+
NOT_FOUND: 'Not found',
|
|
6
|
+
SEED_NOT_FOUND: 'Seed not found',
|
|
7
|
+
DATABASE_ERROR: 'Database error',
|
|
8
|
+
SLUG_CONFLICT: 'Slug already exists for this schema',
|
|
9
|
+
SENSITIVE_FIELD_EDIT: 'Cannot edit sensitive fields',
|
|
10
|
+
} as const
|
|
@@ -1,163 +1,153 @@
|
|
|
1
|
-
import { Context } from 'hono'
|
|
2
|
-
import {
|
|
3
|
-
slugify,
|
|
4
|
-
isValidContentStatus,
|
|
5
|
-
validateAndSanitizeSeedPayload,
|
|
6
|
-
SlugConflictError
|
|
7
|
-
} from '@beechcms/core'
|
|
8
|
-
import { applyPrivacy, PrivacyPolicyError } from '../../../shared/apply-policies'
|
|
9
|
-
import { publicProblem } from '../../../public/problem-details'
|
|
10
|
-
import { CONTENT_ERRORS } from '../constants'
|
|
11
|
-
import { logActivity } from '../../../shared/activity-logger'
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
-
|
|
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
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
status:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
delete bodyForData.
|
|
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
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
console.error('Content create error:', error)
|
|
156
|
-
return publicProblem(context, {
|
|
157
|
-
type: 'content-database-error',
|
|
158
|
-
title: 'Internal Server Error',
|
|
159
|
-
status: 500,
|
|
160
|
-
detail: CONTENT_ERRORS.DATABASE_ERROR
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
}
|
|
1
|
+
import { Context } from 'hono'
|
|
2
|
+
import {
|
|
3
|
+
slugify,
|
|
4
|
+
isValidContentStatus,
|
|
5
|
+
validateAndSanitizeSeedPayload,
|
|
6
|
+
SlugConflictError
|
|
7
|
+
} from '@beechcms/core'
|
|
8
|
+
import { applyPrivacy, PrivacyPolicyError } from '../../../shared/apply-policies'
|
|
9
|
+
import { publicProblem } from '../../../public/problem-details'
|
|
10
|
+
import { CONTENT_ERRORS } from '../constants'
|
|
11
|
+
import { logActivity } from '../../../shared/activity-logger'
|
|
12
|
+
import { cleanStr } from '../../../shared/query-utils'
|
|
13
|
+
import { AppEnv } from '../../../types'
|
|
14
|
+
|
|
15
|
+
function normalizeBody(raw: unknown): Record<string, unknown> {
|
|
16
|
+
return typeof raw === 'object' && raw !== null ? (raw as Record<string, unknown>) : {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function contentValidationProblem(
|
|
20
|
+
context: Context,
|
|
21
|
+
details: Array<{ field: string; expected: string; received: string; message: string }>
|
|
22
|
+
) {
|
|
23
|
+
return publicProblem(context, {
|
|
24
|
+
type: 'content-validation-failed',
|
|
25
|
+
title: 'Bad Request',
|
|
26
|
+
status: 400,
|
|
27
|
+
detail: 'Validation failed',
|
|
28
|
+
errors: details
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createHandler(context: Context<AppEnv>) {
|
|
33
|
+
const slug = context.req.param('slug')
|
|
34
|
+
if (!slug) {
|
|
35
|
+
return publicProblem(context, {
|
|
36
|
+
type: 'content-invalid-slug',
|
|
37
|
+
title: 'Bad Request',
|
|
38
|
+
status: 400,
|
|
39
|
+
detail: CONTENT_ERRORS.INVALID_SLUG
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const seed = context.get('getSeed')(slug)
|
|
44
|
+
if (!seed) {
|
|
45
|
+
return publicProblem(context, {
|
|
46
|
+
type: 'content-seed-not-found',
|
|
47
|
+
title: 'Not Found',
|
|
48
|
+
status: 404,
|
|
49
|
+
detail: CONTENT_ERRORS.SEED_NOT_FOUND
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let body: Record<string, unknown>
|
|
54
|
+
try {
|
|
55
|
+
body = normalizeBody(await context.req.json<unknown>())
|
|
56
|
+
} catch {
|
|
57
|
+
return publicProblem(context, {
|
|
58
|
+
type: 'content-invalid-json',
|
|
59
|
+
title: 'Bad Request',
|
|
60
|
+
status: 400,
|
|
61
|
+
detail: CONTENT_ERRORS.INVALID_JSON_BODY
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const entrySlug = body.slug ? slugify(String(body.slug)) : null
|
|
66
|
+
const status = cleanStr(body.status) ?? 'draft'
|
|
67
|
+
if (!isValidContentStatus(status)) {
|
|
68
|
+
return publicProblem(context, {
|
|
69
|
+
type: 'content-invalid-status',
|
|
70
|
+
title: 'Bad Request',
|
|
71
|
+
status: 400,
|
|
72
|
+
detail: 'Invalid status. Allowed values are: draft, review, published'
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const bodyForData = { ...body }
|
|
77
|
+
delete bodyForData.slug
|
|
78
|
+
delete bodyForData.status
|
|
79
|
+
|
|
80
|
+
const validation = validateAndSanitizeSeedPayload(seed, bodyForData, {
|
|
81
|
+
operation: 'create',
|
|
82
|
+
allowNull: false,
|
|
83
|
+
requireAtLeastOneValidField: true,
|
|
84
|
+
enforceRequiredFields: true,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (validation.dangerousFields.length > 0) {
|
|
88
|
+
return publicProblem(context, {
|
|
89
|
+
type: 'content-dangerous-content',
|
|
90
|
+
title: 'Unprocessable Entity',
|
|
91
|
+
status: 422,
|
|
92
|
+
detail: `Content rejected: dangerous markup detected in field '${validation.dangerousFields[0]}'`
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (validation.details.length > 0) return contentValidationProblem(context, validation.details)
|
|
97
|
+
|
|
98
|
+
let privacyData: Record<string, unknown>
|
|
99
|
+
try {
|
|
100
|
+
privacyData = await applyPrivacy(validation.data, seed)
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof PrivacyPolicyError) {
|
|
103
|
+
return publicProblem(context, {
|
|
104
|
+
type: 'content-policy-not-implemented',
|
|
105
|
+
title: 'Not Implemented',
|
|
106
|
+
status: 501,
|
|
107
|
+
detail: error.message
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
throw error
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const id = crypto.randomUUID()
|
|
114
|
+
let finalSlug = entrySlug
|
|
115
|
+
if (!finalSlug) {
|
|
116
|
+
const fallbackSource = privacyData[seed.displayNameAlias ?? 'title'] || privacyData.title || privacyData.name || id
|
|
117
|
+
finalSlug = slugify(String(fallbackSource))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const repository = context.get('repository')
|
|
122
|
+
await repository.create(seed, id, finalSlug, status, privacyData)
|
|
123
|
+
|
|
124
|
+
const userId = context.get('jwtPayload')?.sub
|
|
125
|
+
const title = privacyData.title || privacyData.name || finalSlug
|
|
126
|
+
|
|
127
|
+
logActivity(context, {
|
|
128
|
+
action: 'create',
|
|
129
|
+
entityType: 'content',
|
|
130
|
+
entityId: id,
|
|
131
|
+
entitySlug: slug,
|
|
132
|
+
details: { title }
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
return context.json({ id }, 201)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof SlugConflictError) {
|
|
138
|
+
return publicProblem(context, {
|
|
139
|
+
type: 'content-slug-conflict',
|
|
140
|
+
title: 'Conflict',
|
|
141
|
+
status: 409,
|
|
142
|
+
detail: CONTENT_ERRORS.SLUG_CONFLICT
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
console.error('Content create error:', error)
|
|
146
|
+
return publicProblem(context, {
|
|
147
|
+
type: 'content-database-error',
|
|
148
|
+
title: 'Internal Server Error',
|
|
149
|
+
status: 500,
|
|
150
|
+
detail: CONTENT_ERRORS.DATABASE_ERROR
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -1,85 +1,76 @@
|
|
|
1
|
-
import { Context } from 'hono'
|
|
2
|
-
import { EntryNotFoundError } from '@beechcms/core'
|
|
3
|
-
import { deleteR2Objects } from '../../../upload'
|
|
4
|
-
import { extractMediaKeysFromData } from '../../../media-utils'
|
|
5
|
-
import { publicProblem } from '../../../public/problem-details'
|
|
6
|
-
import { CONTENT_ERRORS } from '../constants'
|
|
7
|
-
import { logActivity } from '../../../shared/activity-logger'
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
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
|
-
const
|
|
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
|
-
console.error('Content delete error:', error)
|
|
78
|
-
return publicProblem(context, {
|
|
79
|
-
type: 'content-database-error',
|
|
80
|
-
title: 'Internal Server Error',
|
|
81
|
-
status: 500,
|
|
82
|
-
detail: CONTENT_ERRORS.DATABASE_ERROR
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
}
|
|
1
|
+
import { Context } from 'hono'
|
|
2
|
+
import { EntryNotFoundError } from '@beechcms/core'
|
|
3
|
+
import { deleteR2Objects } from '../../../upload'
|
|
4
|
+
import { extractMediaKeysFromData } from '../../../media-utils'
|
|
5
|
+
import { publicProblem } from '../../../public/problem-details'
|
|
6
|
+
import { CONTENT_ERRORS } from '../constants'
|
|
7
|
+
import { logActivity } from '../../../shared/activity-logger'
|
|
8
|
+
import { AppEnv } from '../../../types'
|
|
9
|
+
|
|
10
|
+
export async function deleteHandler(context: Context<AppEnv>) {
|
|
11
|
+
const schemaSlug = context.req.param('slug')
|
|
12
|
+
const entryId = context.req.param('id')
|
|
13
|
+
if (!schemaSlug || !entryId) {
|
|
14
|
+
return publicProblem(context, {
|
|
15
|
+
type: 'content-invalid-slug-or-id',
|
|
16
|
+
title: 'Bad Request',
|
|
17
|
+
status: 400,
|
|
18
|
+
detail: CONTENT_ERRORS.INVALID_SLUG_OR_ID
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const seed = context.get('getSeed')(schemaSlug)
|
|
23
|
+
if (!seed) {
|
|
24
|
+
return publicProblem(context, {
|
|
25
|
+
type: 'content-seed-not-found',
|
|
26
|
+
title: 'Not Found',
|
|
27
|
+
status: 404,
|
|
28
|
+
detail: CONTENT_ERRORS.SEED_NOT_FOUND
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const repository = context.get('repository')
|
|
34
|
+
// Repository.delete returns the row data for cleanup
|
|
35
|
+
const { row } = await repository.delete(seed, entryId)
|
|
36
|
+
|
|
37
|
+
const userId = context.get('jwtPayload')?.sub
|
|
38
|
+
const title = row.title || row.name || entryId
|
|
39
|
+
|
|
40
|
+
logActivity(context, {
|
|
41
|
+
action: 'delete',
|
|
42
|
+
entityType: 'content',
|
|
43
|
+
entityId: entryId,
|
|
44
|
+
entitySlug: schemaSlug,
|
|
45
|
+
details: { title }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
const r2ObjectKeys = extractMediaKeysFromData(seed, row)
|
|
50
|
+
if (r2ObjectKeys.length > 0) {
|
|
51
|
+
await deleteR2Objects(context, r2ObjectKeys).catch((error) => {
|
|
52
|
+
if (context.env.ENV !== 'production') {
|
|
53
|
+
console.warn('R2 cleanup on delete failed (orphaned files):', error)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return context.json({ success: true })
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof EntryNotFoundError) {
|
|
61
|
+
return publicProblem(context, {
|
|
62
|
+
type: 'content-not-found',
|
|
63
|
+
title: 'Not Found',
|
|
64
|
+
status: 404,
|
|
65
|
+
detail: CONTENT_ERRORS.NOT_FOUND
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
console.error('Content delete error:', error)
|
|
69
|
+
return publicProblem(context, {
|
|
70
|
+
type: 'content-database-error',
|
|
71
|
+
title: 'Internal Server Error',
|
|
72
|
+
status: 500,
|
|
73
|
+
detail: CONTENT_ERRORS.DATABASE_ERROR
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import { Context } from 'hono'
|
|
2
|
-
import { publicProblem } from '../../../public/problem-details'
|
|
3
|
-
import { CONTENT_ERRORS } from '../constants'
|
|
4
|
-
import { AppEnv } from '../../../types'
|
|
5
|
-
|
|
6
|
-
export async function facetsHandler(context: Context<AppEnv>) {
|
|
7
|
-
const slug = context.req.param('slug')
|
|
8
|
-
if (!slug) {
|
|
9
|
-
return publicProblem(context, {
|
|
10
|
-
type: 'content-invalid-slug',
|
|
11
|
-
title: 'Bad Request',
|
|
12
|
-
status: 400,
|
|
13
|
-
detail: CONTENT_ERRORS.INVALID_SLUG
|
|
14
|
-
})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const seed = context.get('getSeed')(slug)
|
|
18
|
-
if (!seed) {
|
|
19
|
-
return publicProblem(context, {
|
|
20
|
-
type: 'content-seed-not-found',
|
|
21
|
-
title: 'Not Found',
|
|
22
|
-
status: 404,
|
|
23
|
-
detail: CONTENT_ERRORS.SEED_NOT_FOUND
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const repository = context.get('repository')
|
|
29
|
-
const { statuses, tagsByColumn } = await repository.getFacets(seed)
|
|
30
|
-
|
|
31
|
-
// Legacy format expected by the dashboard
|
|
32
|
-
return context.json({
|
|
33
|
-
statuses: Object.keys(statuses).sort((a, b) => a.localeCompare(b, 'it')),
|
|
34
|
-
tagsByColumnId: tagsByColumn,
|
|
35
|
-
})
|
|
36
|
-
} catch (error) {
|
|
37
|
-
console.error('Content facets error:', error)
|
|
38
|
-
return publicProblem(context, {
|
|
39
|
-
type: 'content-database-error',
|
|
40
|
-
title: 'Internal Server Error',
|
|
41
|
-
status: 500,
|
|
42
|
-
detail: CONTENT_ERRORS.DATABASE_ERROR
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
import { Context } from 'hono'
|
|
2
|
+
import { publicProblem } from '../../../public/problem-details'
|
|
3
|
+
import { CONTENT_ERRORS } from '../constants'
|
|
4
|
+
import { AppEnv } from '../../../types'
|
|
5
|
+
|
|
6
|
+
export async function facetsHandler(context: Context<AppEnv>) {
|
|
7
|
+
const slug = context.req.param('slug')
|
|
8
|
+
if (!slug) {
|
|
9
|
+
return publicProblem(context, {
|
|
10
|
+
type: 'content-invalid-slug',
|
|
11
|
+
title: 'Bad Request',
|
|
12
|
+
status: 400,
|
|
13
|
+
detail: CONTENT_ERRORS.INVALID_SLUG
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const seed = context.get('getSeed')(slug)
|
|
18
|
+
if (!seed) {
|
|
19
|
+
return publicProblem(context, {
|
|
20
|
+
type: 'content-seed-not-found',
|
|
21
|
+
title: 'Not Found',
|
|
22
|
+
status: 404,
|
|
23
|
+
detail: CONTENT_ERRORS.SEED_NOT_FOUND
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const repository = context.get('repository')
|
|
29
|
+
const { statuses, tagsByColumn } = await repository.getFacets(seed)
|
|
30
|
+
|
|
31
|
+
// Legacy format expected by the dashboard
|
|
32
|
+
return context.json({
|
|
33
|
+
statuses: Object.keys(statuses).sort((a, b) => a.localeCompare(b, 'it')),
|
|
34
|
+
tagsByColumnId: tagsByColumn,
|
|
35
|
+
})
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Content facets error:', error)
|
|
38
|
+
return publicProblem(context, {
|
|
39
|
+
type: 'content-database-error',
|
|
40
|
+
title: 'Internal Server Error',
|
|
41
|
+
status: 500,
|
|
42
|
+
detail: CONTENT_ERRORS.DATABASE_ERROR
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|