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