@beechcms/api 0.4.0-preview.10 → 0.4.0-preview.12
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/assets/index-CTSuGxlX.js +554 -0
- package/assets/dashboard/assets/index-ye3325L9.css +1 -0
- package/assets/dashboard/index.html +2 -2
- package/package.json +2 -2
- package/src/factory.ts +96 -85
- package/src/features/content/constants.ts +10 -0
- package/src/features/content/handlers/create.ts +163 -0
- package/src/features/content/handlers/delete.ts +85 -0
- package/src/features/content/handlers/facets.ts +45 -0
- package/src/features/content/handlers/get.ts +116 -0
- package/src/features/content/handlers/list.ts +88 -0
- package/src/features/content/handlers/update.ts +216 -0
- package/src/features/content/index.ts +20 -0
- package/src/features/draft/draft.handler.ts +203 -129
- package/src/features/settings/settings.handler.ts +18 -0
- package/src/index.ts +16 -3
- package/src/middleware/repository.middleware.ts +18 -0
- package/src/public/public-add.ts +72 -89
- package/src/public/public-edit.ts +51 -76
- package/src/public/public-read.ts +113 -114
- package/src/public/query-builder.ts +47 -136
- package/src/shared/base.repository.d1.ts +28 -0
- package/src/shared/content.repository.d1.ts +382 -0
- package/src/shared/idempotency.repository.d1.ts +45 -0
- package/src/types.ts +6 -1
- package/src/upload.ts +3 -7
- package/assets/dashboard/assets/index-CFTJe1vb.js +0 -554
- package/assets/dashboard/assets/index-CQODXprH.css +0 -1
- package/src/content.ts +0 -502
- package/src/features/draft/draft.test.ts +0 -315
- package/src/features/rotate-field/rotate-field.test.ts +0 -297
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import {
|
|
3
|
+
ContentRepository,
|
|
4
|
+
EntryNotFoundError,
|
|
5
|
+
RepositoryError,
|
|
6
|
+
SlugConflictError,
|
|
7
|
+
Seed,
|
|
8
|
+
SelectOptions,
|
|
9
|
+
buildSelectQuery,
|
|
10
|
+
deserializeFromDb,
|
|
11
|
+
serializeForDb,
|
|
12
|
+
} from '@beechcms/core'
|
|
13
|
+
import { BaseD1Repository } from './base.repository.d1'
|
|
14
|
+
|
|
15
|
+
export class D1ContentRepository extends BaseD1Repository implements ContentRepository {
|
|
16
|
+
/**
|
|
17
|
+
* Helper to deserialize a DB row using the Seed's branch definitions.
|
|
18
|
+
*/
|
|
19
|
+
private rowToData(seed: Seed, row: any): Record<string, any> {
|
|
20
|
+
const data: Record<string, any> = {
|
|
21
|
+
id: row.id,
|
|
22
|
+
slug: row.slug,
|
|
23
|
+
status: row.status,
|
|
24
|
+
created_at: row.created_at,
|
|
25
|
+
updated_at: row.updated_at,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const branch of seed.branches) {
|
|
29
|
+
if (Object.hasOwn(row, branch.alias)) {
|
|
30
|
+
data[branch.alias] = deserializeFromDb(branch, row[branch.alias])
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return data
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async findMany(
|
|
38
|
+
seed: Seed,
|
|
39
|
+
options: SelectOptions
|
|
40
|
+
): Promise<{ items: Record<string, any>[]; total: number }> {
|
|
41
|
+
try {
|
|
42
|
+
const { sql, bindings } = buildSelectQuery(seed, options)
|
|
43
|
+
|
|
44
|
+
// We need the total count for pagination.
|
|
45
|
+
// We build a count query by replacing the SELECT part.
|
|
46
|
+
// Note: buildSelectQuery might have joins and where clauses.
|
|
47
|
+
const countSql = sql
|
|
48
|
+
.replace(/SELECT .* FROM/, 'SELECT COUNT(*) as total FROM')
|
|
49
|
+
.replace(/ ORDER BY .*$/, '')
|
|
50
|
+
.replace(/ LIMIT \? OFFSET \?$/, '')
|
|
51
|
+
|
|
52
|
+
const countBindings = bindings.slice(0, bindings.length - (options.pagination ? 2 : 0))
|
|
53
|
+
|
|
54
|
+
const [results, countResult] = await this.db.batch([
|
|
55
|
+
this.db.prepare(sql).bind(...bindings),
|
|
56
|
+
this.db.prepare(countSql).bind(...countBindings)
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
const items = (results.results || []).map((row) => this.rowToData(seed, row))
|
|
60
|
+
const total = (countResult.results?.[0] as any)?.total || 0
|
|
61
|
+
|
|
62
|
+
return { items, total }
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw this.mapError(error, `findMany(${seed.slug})`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async findById(seed: Seed, id: string): Promise<Record<string, any>> {
|
|
69
|
+
try {
|
|
70
|
+
const table = this.getTableName(seed.slug)
|
|
71
|
+
const row = await this.db
|
|
72
|
+
.prepare(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`)
|
|
73
|
+
.bind(id)
|
|
74
|
+
.first()
|
|
75
|
+
|
|
76
|
+
if (!row) {
|
|
77
|
+
throw new EntryNotFoundError(`Entry ${id} not found in ${seed.slug}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return this.rowToData(seed, row)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error instanceof EntryNotFoundError) throw error
|
|
83
|
+
throw this.mapError(error, `findById(${seed.slug}, ${id})`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async findBySlug(seed: Seed, slug: string): Promise<Record<string, any>> {
|
|
88
|
+
try {
|
|
89
|
+
const table = this.getTableName(seed.slug)
|
|
90
|
+
const row = await this.db
|
|
91
|
+
.prepare(`SELECT * FROM ${table} WHERE slug = ? LIMIT 1`)
|
|
92
|
+
.bind(slug)
|
|
93
|
+
.first()
|
|
94
|
+
|
|
95
|
+
if (!row) {
|
|
96
|
+
throw new EntryNotFoundError(`Entry with slug "${slug}" not found in ${seed.slug}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return this.rowToData(seed, row)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof EntryNotFoundError) throw error
|
|
102
|
+
throw this.mapError(error, `findBySlug(${seed.slug}, ${slug})`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getFacets(seed: Seed): Promise<{
|
|
107
|
+
statuses: Record<string, number>
|
|
108
|
+
tagsByColumn: Record<string, string[]>
|
|
109
|
+
}> {
|
|
110
|
+
try {
|
|
111
|
+
const table = this.getTableName(seed.slug)
|
|
112
|
+
|
|
113
|
+
// Status counts
|
|
114
|
+
const statusResults = await this.db
|
|
115
|
+
.prepare(`SELECT status, COUNT(*) as count FROM ${table} GROUP BY status`)
|
|
116
|
+
.all()
|
|
117
|
+
|
|
118
|
+
const statuses: Record<string, number> = {}
|
|
119
|
+
for (const row of statusResults.results || []) {
|
|
120
|
+
statuses[row.status as string] = row.count as number
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Tags facets (for branches of type 'tags')
|
|
124
|
+
const tagsByColumn: Record<string, string[]> = {}
|
|
125
|
+
const tagBranches = seed.branches.filter(b => b.type === 'tags')
|
|
126
|
+
|
|
127
|
+
for (const branch of tagBranches) {
|
|
128
|
+
// SQLite json_each for tags stored as JSON arrays
|
|
129
|
+
const tagResults = await this.db
|
|
130
|
+
.prepare(`SELECT DISTINCT value FROM ${table}, json_each(${table}.${branch.alias}) WHERE value IS NOT NULL`)
|
|
131
|
+
.all()
|
|
132
|
+
tagsByColumn[branch.alias] = (tagResults.results || []).map(r => r.value as string)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { statuses, tagsByColumn }
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw this.mapError(error, `getFacets(${seed.slug})`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async existsSlug(seed: Seed, slug: string, excludeId?: string): Promise<boolean> {
|
|
142
|
+
try {
|
|
143
|
+
const table = this.getTableName(seed.slug)
|
|
144
|
+
let sql = `SELECT 1 FROM ${table} WHERE slug = ?`
|
|
145
|
+
const bindings: any[] = [slug]
|
|
146
|
+
|
|
147
|
+
if (excludeId) {
|
|
148
|
+
sql += ` AND id != ?`
|
|
149
|
+
bindings.push(excludeId)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const row = await this.db.prepare(sql).bind(...bindings).first()
|
|
153
|
+
return row !== null
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw this.mapError(error, `existsSlug(${seed.slug}, ${slug})`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async create(
|
|
160
|
+
seed: Seed,
|
|
161
|
+
id: string,
|
|
162
|
+
slug: string,
|
|
163
|
+
status: string,
|
|
164
|
+
data: Record<string, any>
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
try {
|
|
167
|
+
if (await this.existsSlug(seed, slug)) {
|
|
168
|
+
throw new SlugConflictError(`Slug "${slug}" already exists for ${seed.slug}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const table = this.getTableName(seed.slug)
|
|
172
|
+
const cols = ['id', 'slug', 'status']
|
|
173
|
+
const placeholders = ['?', '?', '?']
|
|
174
|
+
const bindings: any[] = [id, slug, status]
|
|
175
|
+
|
|
176
|
+
for (const branch of seed.branches) {
|
|
177
|
+
if (Object.hasOwn(data, branch.alias)) {
|
|
178
|
+
cols.push(branch.alias)
|
|
179
|
+
placeholders.push('?')
|
|
180
|
+
bindings.push(serializeForDb(branch, data[branch.alias]))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const sql = `INSERT INTO ${table} (${cols.join(', ')}) VALUES (${placeholders.join(', ')})`
|
|
185
|
+
await this.db.prepare(sql).bind(...bindings).run()
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof SlugConflictError) throw error
|
|
188
|
+
throw this.mapError(error, `create(${seed.slug})`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async update(
|
|
193
|
+
seed: Seed,
|
|
194
|
+
id: string,
|
|
195
|
+
data: Record<string, any>,
|
|
196
|
+
status?: string
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
try {
|
|
199
|
+
const table = this.getTableName(seed.slug)
|
|
200
|
+
const setParts: string[] = []
|
|
201
|
+
const bindings: any[] = []
|
|
202
|
+
|
|
203
|
+
if (status) {
|
|
204
|
+
setParts.push('status = ?')
|
|
205
|
+
bindings.push(status)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const branch of seed.branches) {
|
|
209
|
+
if (Object.hasOwn(data, branch.alias)) {
|
|
210
|
+
setParts.push(`${branch.alias} = ?`)
|
|
211
|
+
bindings.push(serializeForDb(branch, data[branch.alias]))
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (setParts.length === 0) return
|
|
216
|
+
|
|
217
|
+
setParts.push('updated_at = (unixepoch())')
|
|
218
|
+
|
|
219
|
+
const sql = `UPDATE ${table} SET ${setParts.join(', ')} WHERE id = ?`
|
|
220
|
+
bindings.push(id)
|
|
221
|
+
|
|
222
|
+
const result = await this.db.prepare(sql).bind(...bindings).run()
|
|
223
|
+
if (result.meta.changes === 0) {
|
|
224
|
+
throw new EntryNotFoundError(`Entry ${id} not found in ${seed.slug}`)
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof EntryNotFoundError) throw error
|
|
228
|
+
throw this.mapError(error, `update(${seed.slug}, ${id})`)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async delete(seed: Seed, id: string): Promise<{ row: Record<string, any> }> {
|
|
233
|
+
try {
|
|
234
|
+
const table = this.getTableName(seed.slug)
|
|
235
|
+
|
|
236
|
+
// We need to return the row for R2 cleanup
|
|
237
|
+
const row = await this.db
|
|
238
|
+
.prepare(`SELECT * FROM ${table} WHERE id = ?`)
|
|
239
|
+
.bind(id)
|
|
240
|
+
.first()
|
|
241
|
+
|
|
242
|
+
if (!row) {
|
|
243
|
+
throw new EntryNotFoundError(`Entry ${id} not found in ${seed.slug}`)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await this.db.prepare(`DELETE FROM ${table} WHERE id = ?`).bind(id).run()
|
|
247
|
+
|
|
248
|
+
return { row: this.rowToData(seed, row) }
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof EntryNotFoundError) throw error
|
|
251
|
+
throw this.mapError(error, `delete(${seed.slug}, ${id})`)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async saveDraft(seed: Seed, entryId: string, data: Record<string, any>): Promise<void> {
|
|
256
|
+
try {
|
|
257
|
+
if (!seed.allowDrafts) {
|
|
258
|
+
throw new RepositoryError(`Drafts not allowed for ${seed.slug}`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const table = this.getTableName(seed.slug, true)
|
|
262
|
+
const cols = ['entry_id']
|
|
263
|
+
const placeholders = ['?']
|
|
264
|
+
const bindings: any[] = [entryId]
|
|
265
|
+
const updateParts: string[] = []
|
|
266
|
+
|
|
267
|
+
for (const branch of seed.branches) {
|
|
268
|
+
if (Object.hasOwn(data, branch.alias)) {
|
|
269
|
+
const val = serializeForDb(branch, data[branch.alias])
|
|
270
|
+
cols.push(branch.alias)
|
|
271
|
+
placeholders.push('?')
|
|
272
|
+
bindings.push(val)
|
|
273
|
+
updateParts.push(`${branch.alias} = EXCLUDED.${branch.alias}`)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
updateParts.push('updated_at = (unixepoch())')
|
|
278
|
+
|
|
279
|
+
const sql = `
|
|
280
|
+
INSERT INTO ${table} (${cols.join(', ')})
|
|
281
|
+
VALUES (${placeholders.join(', ')})
|
|
282
|
+
ON CONFLICT(entry_id) DO UPDATE SET ${updateParts.join(', ')}
|
|
283
|
+
`
|
|
284
|
+
await this.db.prepare(sql).bind(...bindings).run()
|
|
285
|
+
} catch (error) {
|
|
286
|
+
throw this.mapError(error, `saveDraft(${seed.slug}, ${entryId})`)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async getDraft(seed: Seed, entryId: string): Promise<Record<string, any> | null> {
|
|
291
|
+
try {
|
|
292
|
+
if (!seed.allowDrafts) return null
|
|
293
|
+
|
|
294
|
+
const table = this.getTableName(seed.slug, true)
|
|
295
|
+
const row = await this.db
|
|
296
|
+
.prepare(`SELECT * FROM ${table} WHERE entry_id = ?`)
|
|
297
|
+
.bind(entryId)
|
|
298
|
+
.first()
|
|
299
|
+
|
|
300
|
+
if (!row) return null
|
|
301
|
+
|
|
302
|
+
// Filter out nulls from draft row (only include provided fields)
|
|
303
|
+
const data: Record<string, any> = {}
|
|
304
|
+
for (const branch of seed.branches) {
|
|
305
|
+
if (row[branch.alias] !== null) {
|
|
306
|
+
data[branch.alias] = deserializeFromDb(branch, row[branch.alias])
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return data
|
|
311
|
+
} catch (error) {
|
|
312
|
+
throw this.mapError(error, `getDraft(${seed.slug}, ${entryId})`)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async hasDraft(seed: Seed, entryId: string): Promise<boolean> {
|
|
317
|
+
try {
|
|
318
|
+
if (!seed.allowDrafts) return false
|
|
319
|
+
const table = this.getTableName(seed.slug, true)
|
|
320
|
+
const row = await this.db
|
|
321
|
+
.prepare(`SELECT 1 FROM ${table} WHERE entry_id = ? LIMIT 1`)
|
|
322
|
+
.bind(entryId)
|
|
323
|
+
.first()
|
|
324
|
+
return row !== null
|
|
325
|
+
} catch (error) {
|
|
326
|
+
throw this.mapError(error, `hasDraft(${seed.slug}, ${entryId})`)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async publishDraft(seed: Seed, entryId: string): Promise<void> {
|
|
331
|
+
try {
|
|
332
|
+
if (!seed.allowDrafts) return
|
|
333
|
+
|
|
334
|
+
const draftTable = this.getTableName(seed.slug, true)
|
|
335
|
+
const liveTable = this.getTableName(seed.slug)
|
|
336
|
+
|
|
337
|
+
const draftRow = await this.db
|
|
338
|
+
.prepare(`SELECT * FROM ${draftTable} WHERE entry_id = ?`)
|
|
339
|
+
.bind(entryId)
|
|
340
|
+
.first()
|
|
341
|
+
|
|
342
|
+
if (!draftRow) {
|
|
343
|
+
throw new EntryNotFoundError(`No draft found for ${entryId} in ${seed.slug}`)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Build UPDATE for live table
|
|
347
|
+
const setParts: string[] = []
|
|
348
|
+
const bindings: any[] = []
|
|
349
|
+
|
|
350
|
+
for (const branch of seed.branches) {
|
|
351
|
+
if (draftRow[branch.alias] !== null) {
|
|
352
|
+
setParts.push(`${branch.alias} = ?`)
|
|
353
|
+
bindings.push(draftRow[branch.alias])
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
setParts.push('updated_at = (unixepoch())')
|
|
358
|
+
|
|
359
|
+
const updateSql = `UPDATE ${liveTable} SET ${setParts.join(', ')} WHERE id = ?`
|
|
360
|
+
bindings.push(entryId)
|
|
361
|
+
|
|
362
|
+
// Atomic batch
|
|
363
|
+
await this.db.batch([
|
|
364
|
+
this.db.prepare(updateSql).bind(...bindings),
|
|
365
|
+
this.db.prepare(`DELETE FROM ${draftTable} WHERE entry_id = ?`).bind(entryId)
|
|
366
|
+
])
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error instanceof EntryNotFoundError) throw error
|
|
369
|
+
throw this.mapError(error, `publishDraft(${seed.slug}, ${entryId})`)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async deleteDraft(seed: Seed, entryId: string): Promise<void> {
|
|
374
|
+
try {
|
|
375
|
+
if (!seed.allowDrafts) return
|
|
376
|
+
const table = this.getTableName(seed.slug, true)
|
|
377
|
+
await this.db.prepare(`DELETE FROM ${table} WHERE entry_id = ?`).bind(entryId).run()
|
|
378
|
+
} catch (error) {
|
|
379
|
+
throw this.mapError(error, `deleteDraft(${seed.slug}, ${entryId})`)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { IdempotencyRepository, IdempotencyRecord } from '@beechcms/core'
|
|
2
|
+
import { BaseD1Repository } from './base.repository.d1.js'
|
|
3
|
+
|
|
4
|
+
export class D1IdempotencyRepository extends BaseD1Repository implements IdempotencyRepository {
|
|
5
|
+
async lookup(key: string): Promise<IdempotencyRecord | null> {
|
|
6
|
+
const row = await this.db.prepare(
|
|
7
|
+
`SELECT idempotency_key, request_fingerprint, response_status, response_body, expires_at
|
|
8
|
+
FROM public_idempotency_keys WHERE idempotency_key = ? LIMIT 1`
|
|
9
|
+
).bind(key).first<any>()
|
|
10
|
+
|
|
11
|
+
if (!row) return null
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
key: row.idempotency_key,
|
|
15
|
+
fingerprint: row.request_fingerprint,
|
|
16
|
+
responseStatus: row.response_status,
|
|
17
|
+
responseBody: row.response_body,
|
|
18
|
+
expiresAt: row.expires_at
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async store(record: IdempotencyRecord): Promise<void> {
|
|
23
|
+
await this.db.prepare(
|
|
24
|
+
`INSERT INTO public_idempotency_keys (idempotency_key, request_fingerprint, response_status, response_body, created_at, expires_at)
|
|
25
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
26
|
+
ON CONFLICT(idempotency_key) DO UPDATE SET
|
|
27
|
+
request_fingerprint = excluded.request_fingerprint,
|
|
28
|
+
response_status = excluded.response_status,
|
|
29
|
+
response_body = excluded.response_body,
|
|
30
|
+
created_at = excluded.created_at,
|
|
31
|
+
expires_at = excluded.expires_at`
|
|
32
|
+
).bind(
|
|
33
|
+
record.key,
|
|
34
|
+
record.fingerprint,
|
|
35
|
+
record.responseStatus,
|
|
36
|
+
record.responseBody,
|
|
37
|
+
Math.floor(Date.now() / 1000),
|
|
38
|
+
record.expiresAt
|
|
39
|
+
).run()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async cleanup(now: number): Promise<void> {
|
|
43
|
+
await this.db.prepare(`DELETE FROM public_idempotency_keys WHERE expires_at < ?`).bind(now).run()
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
import type { Seed } from '@beechcms/core'
|
|
2
|
+
import type { Seed, ContentRepository, IdempotencyRepository } from '@beechcms/core'
|
|
3
3
|
|
|
4
4
|
export interface Env {
|
|
5
5
|
DB: D1Database
|
|
@@ -26,6 +26,7 @@ export interface Env {
|
|
|
26
26
|
FORGOT_PASSWORD_RATE_LIMITER?: RateLimit
|
|
27
27
|
RESET_PASSWORD_RATE_LIMITER?: RateLimit
|
|
28
28
|
ENV?: string
|
|
29
|
+
DATE_FORMAT?: string
|
|
29
30
|
ASSETS?: Fetcher
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -33,4 +34,8 @@ export interface Variables {
|
|
|
33
34
|
jwtPayload: { sub: string; email?: string }
|
|
34
35
|
getSeed: (slug: string) => Seed | null
|
|
35
36
|
seedRegistry: Record<string, Seed>
|
|
37
|
+
repository: ContentRepository
|
|
38
|
+
idempotencyRepository: IdempotencyRepository
|
|
36
39
|
}
|
|
40
|
+
|
|
41
|
+
export type AppEnv = { Bindings: Env; Variables: Variables }
|
package/src/upload.ts
CHANGED
|
@@ -164,9 +164,7 @@ export const uploadRoutes = new Hono<{
|
|
|
164
164
|
}>()
|
|
165
165
|
|
|
166
166
|
/** POST /upload - Carica file su R2, restituisce URL pubblico */
|
|
167
|
-
uploadRoutes.post('/upload', async (c
|
|
168
|
-
await authMiddleware(c.env.JWT_SECRET)(c, next)
|
|
169
|
-
}, async (c) => {
|
|
167
|
+
uploadRoutes.post('/upload', async (c) => {
|
|
170
168
|
try {
|
|
171
169
|
const { R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT, R2_BUCKET_NAME } = c.env
|
|
172
170
|
if (!R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY || !R2_ENDPOINT || !R2_BUCKET_NAME) {
|
|
@@ -267,10 +265,8 @@ uploadRoutes.post('/upload', async (c, next) => {
|
|
|
267
265
|
}
|
|
268
266
|
})
|
|
269
267
|
|
|
270
|
-
/** DELETE /upload/:key - Elimina un file da R2 */
|
|
271
|
-
uploadRoutes.delete('/:key', async (c
|
|
272
|
-
await authMiddleware(c.env.JWT_SECRET)(c, next)
|
|
273
|
-
}, async (c) => {
|
|
268
|
+
/** DELETE /api/upload/:key - Elimina un file da R2 */
|
|
269
|
+
uploadRoutes.delete('/upload/:key', async (c) => {
|
|
274
270
|
const key = c.req.param('key')
|
|
275
271
|
if (!key) return c.json({ error: 'Missing key' }, 400)
|
|
276
272
|
|