@beechcms/api 0.4.0-preview.10 → 0.4.0-preview.11

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.
@@ -1,30 +1,15 @@
1
- import { isValidContentStatus } from '@beechcms/core'
2
- import type { Seed } from '@beechcms/core'
1
+ import { isValidContentStatus, SlugConflictError } from '@beechcms/core'
3
2
  import type { Context } from 'hono'
4
3
  import { cleanStr } from '../shared/query-utils'
5
- import { buildInsertBindings } from '../shared/content-utils'
6
4
  import { checkPublicOperation } from './access-policy'
7
5
  import { publicProblem } from './problem-details'
8
6
  import { generateEntrySlug, slugify } from './slug-utils'
9
7
  import { sanitizePublicPayload } from './sanitize'
10
8
  import { createNotification } from '../shared/notification-service'
9
+ import { AppEnv } from '../types'
11
10
 
12
- type Bindings = {
13
- DB: D1Database
14
- PUBLIC_READ_API_KEY?: string
15
- PUBLIC_WRITE_API_KEY?: string
16
- PUBLIC_IDEMPOTENCY_TTL_SECONDS?: string
17
- ENV?: string
18
- }
19
-
20
- type Variables = {
21
- jwtPayload: { sub: string; email?: string }
22
- getSeed: (slug: string) => Seed | null
23
- seedRegistry: Record<string, Seed>
24
- }
25
-
26
- function errorMessage(c: Context<{ Bindings: Bindings; Variables: Variables }>, err: unknown): string {
27
- if (c.env.ENV !== 'production' && err instanceof Error) return err.message
11
+ function errorMessage(context: Context<AppEnv>, error: unknown): string {
12
+ if (context.env.ENV !== 'production' && error instanceof Error) return error.message
28
13
  return 'An unexpected error occurred.'
29
14
  }
30
15
 
@@ -57,127 +42,125 @@ async function sha256Hex(input: string): Promise<string> {
57
42
  return toHex(digest)
58
43
  }
59
44
 
60
- type StoredIdempotencyRecord = {
61
- idempotency_key: string
62
- request_fingerprint: string
63
- response_status: number
64
- response_body: string
65
- expires_at: number
66
- }
67
-
68
- async function lookupIdempotency(db: D1Database, key: string): Promise<StoredIdempotencyRecord | null> {
69
- return db.prepare(
70
- `SELECT idempotency_key, request_fingerprint, response_status, response_body, expires_at
71
- FROM public_idempotency_keys WHERE idempotency_key = ? LIMIT 1`
72
- ).bind(key).first<StoredIdempotencyRecord>()
73
- }
74
-
75
- async function storeIdempotency(db: D1Database, input: {
76
- key: string; fingerprint: string; responseStatus: number
77
- responseBody: string; expiresAt: number; createdAt: number
78
- }): Promise<void> {
79
- await db.prepare(
80
- `INSERT INTO public_idempotency_keys (idempotency_key, request_fingerprint, response_status, response_body, created_at, expires_at)
81
- VALUES (?, ?, ?, ?, ?, ?)
82
- ON CONFLICT(idempotency_key) DO UPDATE SET
83
- request_fingerprint = excluded.request_fingerprint,
84
- response_status = excluded.response_status,
85
- response_body = excluded.response_body,
86
- created_at = excluded.created_at,
87
- expires_at = excluded.expires_at`
88
- ).bind(input.key, input.fingerprint, input.responseStatus, input.responseBody, input.createdAt, input.expiresAt).run()
89
- }
90
-
91
- export async function publicAddHandler(c: Context<{ Bindings: Bindings; Variables: Variables }>) {
92
- const seedSlug = c.req.param('seed') ?? ''
93
- const seed = c.get('getSeed')(seedSlug)
45
+ export async function publicAddHandler(context: Context<AppEnv>) {
46
+ const seedSlug = context.req.param('seed') ?? ''
47
+ const seed = context.get('getSeed')(seedSlug)
94
48
  if (!seed) {
95
- return publicProblem(c, { type: 'seed-not-found', title: 'Seed Not Found', status: 404, detail: `The content type '${seedSlug}' does not exist.` })
49
+ return publicProblem(context, {
50
+ type: 'seed-not-found',
51
+ title: 'Seed Not Found',
52
+ status: 404,
53
+ detail: `The content type '${seedSlug}' does not exist.`
54
+ })
96
55
  }
56
+
97
57
  const access = checkPublicOperation(seed, 'add')
98
58
  if (!access.ok) {
99
- return publicProblem(c, { type: 'operation-not-allowed', title: access.error.error, status: 403, detail: access.error.message })
59
+ return publicProblem(context, {
60
+ type: 'operation-not-allowed',
61
+ title: access.error.error,
62
+ status: 403,
63
+ detail: access.error.message
64
+ })
100
65
  }
101
66
 
102
67
  let body: Record<string, unknown>
103
68
  try {
104
- const parsed = await c.req.json<unknown>()
69
+ const parsed = await context.req.json<unknown>()
105
70
  body = asRecord(parsed) ?? {}
106
71
  } catch {
107
- return publicProblem(c, { type: 'invalid-json-body', title: 'Bad Request', status: 400, detail: 'Invalid JSON body' })
72
+ return publicProblem(context, { type: 'invalid-json-body', title: 'Bad Request', status: 400, detail: 'Invalid JSON body' })
108
73
  }
109
74
 
110
75
  const rawData = asRecord(body.data)
111
76
  if (!rawData || Object.keys(rawData).length === 0) {
112
- return publicProblem(c, { type: 'invalid-data-object', title: 'Bad Request', status: 400, detail: "Field 'data' is required and must be a non-empty object" })
77
+ return publicProblem(context, { type: 'invalid-data-object', title: 'Bad Request', status: 400, detail: "Field 'data' is required and must be a non-empty object" })
113
78
  }
114
79
 
115
80
  const statusValue = body.status ?? 'draft'
116
81
  if (!isValidContentStatus(statusValue)) {
117
- return publicProblem(c, { type: 'invalid-status', title: 'Bad Request', status: 400, detail: 'Invalid status. Allowed values are: draft, review, published' })
82
+ return publicProblem(context, { type: 'invalid-status', title: 'Bad Request', status: 400, detail: 'Invalid status. Allowed values are: draft, review, published' })
118
83
  }
119
84
 
120
85
  const sanitized = sanitizePublicPayload(seed, rawData, {
121
- operation: 'create', allowNull: false, requireAtLeastOneValidField: true, enforceRequiredFields: true,
86
+ operation: 'create',
87
+ allowNull: false,
88
+ requireAtLeastOneValidField: true,
89
+ enforceRequiredFields: true,
122
90
  })
91
+
123
92
  if (!sanitized.ok) {
124
93
  if (sanitized.status === 422) {
125
- return publicProblem(c, { type: sanitized.code, title: 'Unprocessable Entity', status: 422, detail: sanitized.message })
94
+ return publicProblem(context, { type: sanitized.code, title: 'Unprocessable Entity', status: 422, detail: sanitized.message })
126
95
  }
127
- return publicProblem(c, { type: sanitized.code, title: 'Bad Request', status: 400, detail: sanitized.message, errors: sanitized.details })
96
+ return publicProblem(context, { type: sanitized.code, title: 'Bad Request', status: 400, detail: sanitized.message, errors: sanitized.details })
128
97
  }
129
98
 
130
- const idempotencyKey = parseIdempotencyKey(c.req.header('Idempotency-Key'))
99
+ const idempotencyKey = parseIdempotencyKey(context.req.header('Idempotency-Key'))
131
100
  const entrySlug = pickSlugFromBody(body, sanitized.data)
132
101
  const finalSlug = entrySlug || crypto.randomUUID().slice(0, 8)
133
- const table = `content_${seedSlug}`
102
+ const repository = context.get('repository')
103
+ const idempotencyRepository = context.get('idempotencyRepository')
134
104
 
135
105
  try {
136
- const { DB } = c.env
137
106
  const now = Math.floor(Date.now() / 1000)
138
107
  const fingerprintPayload = JSON.stringify({ seedSlug, statusValue, slug: cleanStr(body.slug) ?? null, data: sanitized.data })
139
108
  const fingerprint = await sha256Hex(fingerprintPayload)
140
- const idempotencyTtlSeconds = Math.max(60, Number.parseInt(c.env.PUBLIC_IDEMPOTENCY_TTL_SECONDS ?? '86400', 10) || 86400)
109
+ const idempotencyTtlSeconds = Math.max(60, Number.parseInt(context.env.PUBLIC_IDEMPOTENCY_TTL_SECONDS ?? '86400', 10) || 86400)
141
110
 
142
111
  if (idempotencyKey) {
143
- const existing = await lookupIdempotency(DB, idempotencyKey)
144
- if (existing && existing.expires_at >= now) {
145
- if (existing.request_fingerprint !== fingerprint) {
146
- return publicProblem(c, { type: 'idempotency-key-conflict', title: 'Conflict', status: 409, detail: 'Idempotency-Key was already used with a different request payload.' })
112
+ const existing = await idempotencyRepository.lookup(idempotencyKey)
113
+ if (existing && existing.expiresAt >= now) {
114
+ if (existing.fingerprint !== fingerprint) {
115
+ return publicProblem(context, { type: 'idempotency-key-conflict', title: 'Conflict', status: 409, detail: 'Idempotency-Key was already used with a different request payload.' })
147
116
  }
148
117
  let parsedBody: unknown = null
149
- try { parsedBody = JSON.parse(existing.response_body) } catch { parsedBody = { success: true } }
150
- return c.json(parsedBody, existing.response_status as 201)
118
+ try { parsedBody = JSON.parse(existing.responseBody) } catch { parsedBody = { success: true } }
119
+ return context.json(parsedBody, existing.responseStatus as 201)
151
120
  }
152
121
  }
153
122
 
154
- const slugExisting = await DB.prepare(`SELECT id FROM ${table} WHERE slug = ? LIMIT 1`).bind(finalSlug).first<{ id: string }>()
155
- if (slugExisting) {
156
- return publicProblem(c, { type: 'slug-conflict', title: 'Conflict', status: 409, detail: `An entry with slug '${finalSlug}' already exists for content type '${seedSlug}'.` })
157
- }
158
-
159
123
  const id = crypto.randomUUID()
160
- const { cols, placeholders, bindings } = buildInsertBindings(seed, sanitized.data)
161
- const systemCols = ['id', 'slug', 'status', 'created_at', 'updated_at']
162
- const systemVals = [id, finalSlug, statusValue, now, now]
163
- const allCols = [...systemCols, ...cols].join(', ')
164
- const allPlaceholders = [...systemVals.map(() => '?'), ...placeholders].join(', ')
165
-
166
- await DB.prepare(`INSERT INTO ${table} (${allCols}) VALUES (${allPlaceholders})`).bind(...systemVals, ...bindings).run()
124
+
125
+ try {
126
+ await repository.create(seed, id, finalSlug, statusValue as any, sanitized.data)
127
+ } catch (error) {
128
+ if (error instanceof SlugConflictError) {
129
+ return publicProblem(context, {
130
+ type: 'slug-conflict',
131
+ title: 'Conflict',
132
+ status: 409,
133
+ detail: `An entry with slug '${finalSlug}' already exists for content type '${seedSlug}'.`
134
+ })
135
+ }
136
+ throw error
137
+ }
167
138
 
168
139
  const responseBody = { success: true, id, slug: finalSlug }
169
140
  if (idempotencyKey) {
170
- await storeIdempotency(DB, { key: idempotencyKey, fingerprint, responseStatus: 201, responseBody: JSON.stringify(responseBody), createdAt: now, expiresAt: now + idempotencyTtlSeconds })
141
+ await idempotencyRepository.store({
142
+ key: idempotencyKey,
143
+ fingerprint,
144
+ responseStatus: 201,
145
+ responseBody: JSON.stringify(responseBody),
146
+ expiresAt: now + idempotencyTtlSeconds
147
+ })
171
148
  }
172
149
 
173
- await createNotification(c, {
150
+ await createNotification(context, {
174
151
  title: `${seed.label}: Nuovo inserimento`,
175
152
  message: `Una nuova entry ("${sanitized.data.title || sanitized.data.name || finalSlug}") è stata aggiunta via API pubblica.`,
176
153
  type: 'success',
177
154
  })
178
155
 
179
- return c.json(responseBody, 201)
180
- } catch (err) {
181
- return publicProblem(c, { type: 'internal-server-error', title: 'Internal Server Error', status: 500, detail: errorMessage(c, err) })
156
+ return context.json(responseBody, 201)
157
+ } catch (error) {
158
+ console.error('Public add error:', error)
159
+ return publicProblem(context, {
160
+ type: 'internal-server-error',
161
+ title: 'Internal Server Error',
162
+ status: 500,
163
+ detail: errorMessage(context, error)
164
+ })
182
165
  }
183
166
  }
@@ -1,33 +1,20 @@
1
- import { isValidContentStatus, resolvePolicies } from '@beechcms/core'
1
+ import { isValidContentStatus, resolvePolicies, EntryNotFoundError } from '@beechcms/core'
2
2
  import type { Seed } from '@beechcms/core'
3
3
  import type { Context } from 'hono'
4
4
  import { cleanStr } from '../shared/query-utils'
5
- import { rowToApiData, buildUpdateBindings } from '../shared/content-utils'
6
5
  import { checkPublicOperation } from './access-policy'
7
6
  import { publicProblem } from './problem-details'
8
7
  import { slugify } from './slug-utils'
9
8
  import { sanitizePublicPayload } from './sanitize'
10
9
  import { createNotification } from '../shared/notification-service'
11
-
12
- type Bindings = {
13
- DB: D1Database
14
- PUBLIC_READ_API_KEY?: string
15
- PUBLIC_WRITE_API_KEY?: string
16
- ENV?: string
17
- }
18
-
19
- type Variables = {
20
- jwtPayload: { sub: string; email?: string }
21
- getSeed: (slug: string) => Seed | null
22
- seedRegistry: Record<string, Seed>
23
- }
10
+ import { AppEnv } from '../types'
24
11
 
25
12
  const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
26
- type PublicCtx = Context<{ Bindings: Bindings; Variables: Variables }>
13
+ type PublicCtx = Context<AppEnv>
27
14
  type ResolveResult<T> = { ok: true; value: T } | { ok: false; response: Response }
28
15
 
29
- function errorMessage(c: PublicCtx, err: unknown): string {
30
- if (c.env.ENV !== 'production' && err instanceof Error) return err.message
16
+ function errorMessage(context: PublicCtx, error: unknown): string {
17
+ if (context.env.ENV !== 'production' && error instanceof Error) return error.message
31
18
  return 'An unexpected error occurred.'
32
19
  }
33
20
 
@@ -45,48 +32,47 @@ function removeNullishFields(data: Record<string, unknown>): Record<string, unkn
45
32
  return next
46
33
  }
47
34
 
48
- function parseBody(c: PublicCtx): Promise<ResolveResult<Record<string, unknown>>> {
49
- return c.req.json<unknown>()
35
+ function parseBody(context: PublicCtx): Promise<ResolveResult<Record<string, unknown>>> {
36
+ return context.req.json<unknown>()
50
37
  .then((parsed) => ({ ok: true, value: asRecord(parsed) ?? {} }) as const)
51
38
  .catch(() => ({
52
39
  ok: false,
53
- response: publicProblem(c, { type: 'invalid-json-body', title: 'Bad Request', status: 400, detail: 'Invalid JSON body' }),
40
+ response: publicProblem(context, { type: 'invalid-json-body', title: 'Bad Request', status: 400, detail: 'Invalid JSON body' }),
54
41
  }))
55
42
  }
56
43
 
57
- function resolveSlug(c: PublicCtx, body: Record<string, unknown>, currentSlug: string): ResolveResult<{ slugRequested: boolean; nextSlug: string }> {
44
+ function resolveSlug(context: PublicCtx, body: Record<string, unknown>, currentSlug: string): ResolveResult<{ slugRequested: boolean; nextSlug: string }> {
58
45
  const slugRequested = Object.hasOwn(body, 'slug')
59
46
  if (!slugRequested) return { ok: true, value: { slugRequested, nextSlug: currentSlug } }
60
47
  const requestedSlug = cleanStr(body.slug)
61
48
  if (!requestedSlug) {
62
- return { ok: false, response: publicProblem(c, { type: 'invalid-slug', title: 'Bad Request', status: 400, detail: "Field 'slug' must be a non-empty string" }) }
49
+ return { ok: false, response: publicProblem(context, { type: 'invalid-slug', title: 'Bad Request', status: 400, detail: "Field 'slug' must be a non-empty string" }) }
63
50
  }
64
51
  return { ok: true, value: { slugRequested, nextSlug: slugify(requestedSlug) } }
65
52
  }
66
53
 
67
- function resolveStatus(c: PublicCtx, body: Record<string, unknown>, currentStatus: string): ResolveResult<string> {
54
+ function resolveStatus(context: PublicCtx, body: Record<string, unknown>, currentStatus: string): ResolveResult<string> {
68
55
  if (!Object.hasOwn(body, 'status')) return { ok: true, value: currentStatus }
69
56
  const statusValue = body.status
70
57
  if (!isValidContentStatus(statusValue)) {
71
- return { ok: false, response: publicProblem(c, { type: 'invalid-status', title: 'Bad Request', status: 400, detail: 'Invalid status. Allowed values are: draft, review, published' }) }
58
+ return { ok: false, response: publicProblem(context, { type: 'invalid-status', title: 'Bad Request', status: 400, detail: 'Invalid status. Allowed values are: draft, review, published' }) }
72
59
  }
73
- return { ok: true, value: statusValue }
60
+ return { ok: true, value: statusValue as string }
74
61
  }
75
62
 
76
63
  function resolveData(
77
- c: PublicCtx,
64
+ context: PublicCtx,
78
65
  seed: Seed,
79
- body: Record<string, unknown>,
80
- currentRow: Record<string, unknown>
66
+ body: Record<string, unknown>
81
67
  ): ResolveResult<Record<string, unknown>> {
82
68
  if (!Object.hasOwn(body, 'data')) {
83
- // No data update — return empty patch (caller keeps existing columns)
69
+ // No data update — return empty patch
84
70
  return { ok: true, value: {} }
85
71
  }
86
72
 
87
73
  const rawData = asRecord(body.data)
88
74
  if (!rawData) {
89
- return { ok: false, response: publicProblem(c, { type: 'invalid-data-object', title: 'Bad Request', status: 400, detail: "Field 'data' must be an object when provided" }) }
75
+ return { ok: false, response: publicProblem(context, { type: 'invalid-data-object', title: 'Bad Request', status: 400, detail: "Field 'data' must be an object when provided" }) }
90
76
  }
91
77
 
92
78
  const sensitiveAliases = Object.keys(rawData).filter((alias) => {
@@ -94,90 +80,79 @@ function resolveData(
94
80
  return branch != null && resolvePolicies(branch).privacy !== 'plain'
95
81
  })
96
82
  if (sensitiveAliases.length > 0) {
97
- return { ok: false, response: publicProblem(c, { type: 'sensitive-field-edit', title: 'Unprocessable Entity', status: 422, detail: `Cannot edit sensitive fields: ${sensitiveAliases.join(', ')}` }) }
83
+ return { ok: false, response: publicProblem(context, { type: 'sensitive-field-edit', title: 'Unprocessable Entity', status: 422, detail: `Cannot edit sensitive fields: ${sensitiveAliases.join(', ')}` }) }
98
84
  }
99
85
 
100
86
  const sanitized = sanitizePublicPayload(seed, rawData, { allowNull: true, operation: 'update', requireAtLeastOneValidField: true, enforceRequiredFields: true })
101
87
  if (!sanitized.ok) {
102
88
  if (sanitized.status === 422) {
103
- return { ok: false, response: publicProblem(c, { type: sanitized.code, title: 'Unprocessable Entity', status: 422, detail: sanitized.message }) }
89
+ return { ok: false, response: publicProblem(context, { type: sanitized.code, title: 'Unprocessable Entity', status: 422, detail: sanitized.message }) }
104
90
  }
105
- return { ok: false, response: publicProblem(c, { type: sanitized.code, title: 'Bad Request', status: 400, detail: sanitized.message, errors: sanitized.details }) }
91
+ return { ok: false, response: publicProblem(context, { type: sanitized.code, title: 'Bad Request', status: 400, detail: sanitized.message, errors: sanitized.details }) }
106
92
  }
107
93
 
108
- // Merge: current columns + patch, remove nulls
109
- const currentAliasData = rowToApiData(seed, currentRow)
110
- const merged = removeNullishFields({ ...currentAliasData, ...sanitized.data })
111
- return { ok: true, value: merged }
94
+ return { ok: true, value: removeNullishFields(sanitized.data) }
112
95
  }
113
96
 
114
- export async function publicEditHandler(c: PublicCtx) {
115
- const seedSlug = c.req.param('seed') ?? ''
116
- const id = c.req.param('id') ?? ''
117
- const seed = c.get('getSeed')(seedSlug)
97
+ export async function publicEditHandler(context: PublicCtx) {
98
+ const seedSlug = context.req.param('seed') ?? ''
99
+ const id = context.req.param('id') ?? ''
100
+ const seed = context.get('getSeed')(seedSlug)
118
101
  if (!seed) {
119
- return publicProblem(c, { type: 'seed-not-found', title: 'Seed Not Found', status: 404, detail: `The content type '${seedSlug}' does not exist.` })
102
+ return publicProblem(context, { type: 'seed-not-found', title: 'Seed Not Found', status: 404, detail: `The content type '${seedSlug}' does not exist.` })
120
103
  }
121
104
  const access = checkPublicOperation(seed, 'edit')
122
105
  if (!access.ok) {
123
- return publicProblem(c, { type: 'operation-not-allowed', title: access.error.error, status: 403, detail: access.error.message })
106
+ return publicProblem(context, { type: 'operation-not-allowed', title: access.error.error, status: 403, detail: access.error.message })
124
107
  }
125
108
 
126
109
  if (!UUID_REGEX.test(id)) {
127
- return publicProblem(c, { type: 'invalid-entry-id', title: 'Bad Request', status: 400, detail: 'Invalid entry ID format' })
110
+ return publicProblem(context, { type: 'invalid-entry-id', title: 'Bad Request', status: 400, detail: 'Invalid entry ID format' })
128
111
  }
129
112
 
130
- const table = `content_${seedSlug}`
113
+ const repository = context.get('repository')
131
114
 
132
115
  try {
133
- const { DB } = c.env
134
- const currentRow = await DB.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`)
135
- .bind(id)
136
- .first<Record<string, unknown>>()
137
-
138
- if (!currentRow) {
139
- return publicProblem(c, { type: 'entry-not-found', title: 'Not Found', status: 404, detail: `Entry '${id}' not found for content type '${seedSlug}'.` })
140
- }
116
+ const entry = await repository.findById(seed, id)
141
117
 
142
- const bodyResult = await parseBody(c)
118
+ const bodyResult = await parseBody(context)
143
119
  if (!bodyResult.ok) return bodyResult.response
144
120
 
145
- const slugResult = resolveSlug(c, bodyResult.value, (currentRow.slug as string) ?? '')
121
+ const slugResult = resolveSlug(context, bodyResult.value, (entry.slug as string) ?? '')
146
122
  if (!slugResult.ok) return slugResult.response
147
123
 
148
- const statusResult = resolveStatus(c, bodyResult.value, (currentRow.status as string) ?? 'draft')
124
+ const statusResult = resolveStatus(context, bodyResult.value, (entry.status as string) ?? 'draft')
149
125
  if (!statusResult.ok) return statusResult.response
150
126
 
151
- const dataResult = resolveData(c, seed, bodyResult.value, currentRow)
127
+ const dataResult = resolveData(context, seed, bodyResult.value)
152
128
  if (!dataResult.ok) return dataResult.response
153
129
 
154
- if (slugResult.value.slugRequested) {
155
- const slugExisting = await DB.prepare(`SELECT id FROM ${table} WHERE slug = ? AND id != ? LIMIT 1`)
156
- .bind(slugResult.value.nextSlug, id)
157
- .first<{ id: string }>()
158
-
159
- if (slugExisting) {
160
- return publicProblem(c, { type: 'slug-conflict', title: 'Conflict', status: 409, detail: `An entry with slug '${slugResult.value.nextSlug}' already exists for content type '${seedSlug}'.` })
130
+ if (slugResult.value.slugRequested && slugResult.value.nextSlug !== entry.slug) {
131
+ const exists = await repository.existsSlug(seed, slugResult.value.nextSlug, id)
132
+ if (exists) {
133
+ return publicProblem(context, { type: 'slug-conflict', title: 'Conflict', status: 409, detail: `An entry with slug '${slugResult.value.nextSlug}' already exists for content type '${seedSlug}'.` })
161
134
  }
162
135
  }
163
136
 
164
- const now = Math.floor(Date.now() / 1000)
165
- const { setClause, bindings } = buildUpdateBindings(seed, dataResult.value)
166
- const systemSet = `slug = ?, status = ?, updated_at = ?`
167
- const fullSet = setClause ? `${systemSet}, ${setClause}` : systemSet
137
+ const updateData = { ...dataResult.value }
138
+ if (slugResult.value.slugRequested) {
139
+ (updateData as any).slug = slugResult.value.nextSlug
140
+ }
168
141
 
169
- await DB.prepare(`UPDATE ${table} SET ${fullSet} WHERE id = ?`)
170
- .bind(slugResult.value.nextSlug, statusResult.value, now, ...bindings, id)
171
- .run()
142
+ await repository.update(seed, id, updateData, statusResult.value)
172
143
 
173
- await createNotification(c, {
144
+ await createNotification(context, {
174
145
  title: `${seed.label}: Modifica`,
175
146
  message: `L'entry "${slugResult.value.nextSlug}" è stata modificata via API pubblica.`,
176
147
  type: 'info',
177
148
  })
178
149
 
179
- return c.json({ success: true, id, slug: slugResult.value.nextSlug }, 200)
180
- } catch (err) {
181
- return publicProblem(c, { type: 'internal-server-error', title: 'Internal Server Error', status: 500, detail: errorMessage(c, err) })
150
+ return context.json({ success: true, id, slug: slugResult.value.nextSlug }, 200)
151
+ } catch (error) {
152
+ if (error instanceof EntryNotFoundError) {
153
+ return publicProblem(context, { type: 'entry-not-found', title: 'Not Found', status: 404, detail: `Entry '${id}' not found for content type '${seedSlug}'.` })
154
+ }
155
+ console.error('Public edit error:', error)
156
+ return publicProblem(context, { type: 'internal-server-error', title: 'Internal Server Error', status: 500, detail: errorMessage(context, error) })
182
157
  }
183
158
  }