Number entries on publish, delete media, read the cover

- drafts stay unnumbered, the number is handed out when an entry goes live
- DELETE /api/v1/media/:id and a delete control in the media library
- cover_media_id in the draft read, coverMediaId and coverUrl in the archive read
This commit is contained in:
Matthias G
2026-08-01 12:25:01 +02:00
parent afaa323848
commit 2fd71fd406
31 changed files with 2286 additions and 76 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ describe('POST /api/v1/posts', () => {
expect(response.status).toBe(201)
expect(body.status).toBe('draft')
expect(body.number).toBe(1)
expect(body.number).toBeNull()
expect(body.slug).toBe('regel-engine')
})
+63 -14
View File
@@ -3,10 +3,12 @@ import { asc, eq } from 'drizzle-orm'
import { db } from '~/data/db'
import { posts } from '~/data/schema'
import {
assignEntryNumber,
createEntry,
listEntries,
listEntryBlocks,
replaceEntryBlocks,
setEntryStatus,
type EntryCreateValues,
} from '~/data/repositories/entries'
import { makeBrand, makeProject } from '../support/admin'
@@ -26,7 +28,7 @@ function values(projectId: string, slug: string, title = 'Beitrag'): EntryCreate
}
}
async function numbersOf(projectId: string): Promise<number[]> {
async function numbersOf(projectId: string): Promise<(number | null)[]> {
const rows = await db
.select({ number: posts.number })
.from(posts)
@@ -36,28 +38,62 @@ async function numbersOf(projectId: string): Promise<number[]> {
return rows.map(row => row.number)
}
describe('createEntry vergibt Beitragsnummern', () => {
it('zählt je Projekt getrennt hoch', async () => {
async function publish(id: string): Promise<number | null> {
const post = await setEntryStatus(id, 'published', new Date())
return post?.number ?? null
}
describe('Beitragsnummern', () => {
it('lässt Entwürfe ohne Nummer', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const created = await createEntry(values(project.id, 'entwurf'))
expect(created.number).toBeNull()
expect(await numbersOf(project.id)).toEqual([null])
})
it('zählt beim Veröffentlichen je Projekt getrennt hoch', async () => {
const brand = await makeBrand()
const trakk = await makeProject(brand.id, 'trakk', 'Trakk')
const orbit = await makeProject(brand.id, 'orbit', 'Orbit')
await createEntry(values(trakk.id, 'eins'))
await createEntry(values(trakk.id, 'zwei'))
await createEntry(values(orbit.id, 'eins'))
const first = await createEntry(values(trakk.id, 'eins'))
const second = await createEntry(values(trakk.id, 'zwei'))
const third = await createEntry(values(orbit.id, 'eins'))
expect(await numbersOf(trakk.id)).toEqual([1, 2])
expect(await numbersOf(orbit.id)).toEqual([1])
expect(await publish(first.id)).toBe(1)
expect(await publish(second.id)).toBe(2)
expect(await publish(third.id)).toBe(1)
})
it('vergibt bei gleichzeitigem Anlegen lückenlose Nummern ohne Doppelung', async () => {
it('lässt keine Lücke, wenn ein Entwurf dazwischen gelöscht wird', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
await Promise.all(
const first = await createEntry(values(project.id, 'eins'))
const scrapped = await createEntry(values(project.id, 'verworfen'))
const second = await createEntry(values(project.id, 'zwei'))
await publish(first.id)
await db.delete(posts).where(eq(posts.id, scrapped.id))
await publish(second.id)
expect(await numbersOf(project.id)).toEqual([1, 2])
})
it('vergibt bei gleichzeitigem Veröffentlichen lückenlose Nummern ohne Doppelung', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const created = await Promise.all(
Array.from({ length: 20 }, (_, index) => createEntry(values(project.id, `beitrag-${index}`))),
)
await Promise.all(created.map(entry => publish(entry.id)))
expect(await numbersOf(project.id)).toEqual(Array.from({ length: 20 }, (_, index) => index + 1))
})
@@ -69,10 +105,23 @@ describe('createEntry vergibt Beitragsnummern', () => {
const created = await createEntry(values(project.id, 'neuer-beitrag'))
expect(created.number).toBe(143)
expect(await publish(created.id)).toBe(143)
})
it('hängt die Nummer an, wenn der Slug im Projekt schon vergeben ist', async () => {
it('vergibt eine bereits gesetzte Nummer nicht neu', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
const created = await createEntry(values(project.id, 'eins'))
await publish(created.id)
const again = await assignEntryNumber(created.id)
expect(again?.number).toBe(1)
})
it('hängt eine Zahl an, wenn der Slug im Projekt schon vergeben ist', async () => {
const brand = await makeBrand()
const project = await makeProject(brand.id)
@@ -80,7 +129,7 @@ describe('createEntry vergibt Beitragsnummern', () => {
const second = await createEntry(values(project.id, 'regel-engine'))
expect(first.slug).toBe('regel-engine')
expect(second.slug).toBe(`regel-engine-${second.number}`)
expect(second.slug).toBe('regel-engine-2')
})
it('übernimmt Titel, Autor und Zielgruppe', async () => {
@@ -94,7 +143,7 @@ describe('createEntry vergibt Beitragsnummern', () => {
authorName: 'Paul',
audience: 'customer',
status: 'draft',
number: 1,
number: null,
})
})
})
+6 -6
View File
@@ -29,7 +29,7 @@ describe('performSaveEntry legt an', () => {
it('speichert Beitrag, Blöcke und Autor', async () => {
const { entry } = await ready()
expect(entry.number).toBe(1)
expect(entry.number).toBeNull()
expect(entry.slug).toBe('regel-engine')
expect(entry.status).toBe('draft')
@@ -91,7 +91,7 @@ describe('performSaveEntry ändert', () => {
expect(state).toMatchObject({ ok: true })
expect(state.ok && state.entry.slug).toBe('regel-engine')
expect(state.ok && state.entry.number).toBe(1)
expect(state.ok && state.entry.number).toBeNull()
})
it('schreibt Blöcke in neuer Reihenfolge', async () => {
@@ -120,7 +120,7 @@ describe('performSaveEntry ändert', () => {
expect(state).toMatchObject({ ok: false, error: 'slugTaken' })
})
it('lässt das Projekt wechseln und nummeriert im Zielprojekt neu', async () => {
it('lässt das Projekt wechseln, ohne dem Entwurf eine Nummer zu geben', async () => {
const brand = await makeBrand()
const trakk = await makeProject(brand.id, 'trakk', 'Trakk')
const orbit = await makeProject(brand.id, 'orbit', 'Orbit')
@@ -136,7 +136,7 @@ describe('performSaveEntry ändert', () => {
const saved = await findEntry(created.ok ? created.entry.id : '')
expect(saved?.projectId).toBe(orbit.id)
expect(saved?.number).toBe(1)
expect(saved?.number).toBeNull()
})
})
@@ -279,7 +279,7 @@ describe('Veröffentlichungsprüfungen', () => {
})
describe('performSaveEntry wechselt das Projekt', () => {
it('vergibt im Zielprojekt die nächste freie Nummer', async () => {
it('lässt die Nummer des Entwurfs im Zielprojekt offen', async () => {
const brand = await makeBrand()
const from = await makeProject(brand.id, 'quelle', 'Quelle')
const to = await makeProject(brand.id, 'ziel', 'Ziel')
@@ -303,7 +303,7 @@ describe('performSaveEntry wechselt das Projekt', () => {
const saved = await findEntry(created.entry.id)
expect(saved?.projectId).toBe(to.id)
expect(moved.entry.number).toBe(2)
expect(moved.entry.number).toBeNull()
})
it('macht den Slug im Zielprojekt eindeutig', async () => {
+1 -1
View File
@@ -25,7 +25,7 @@ describe('seed', () => {
expect.arrayContaining(['trakk', 'mta360', 'mta-telematik', 'orbit', 'pulsar']),
)
expect(postRows.length).toBeGreaterThanOrEqual(6)
expect(postRows.every(post => post.number > 0)).toBe(true)
expect(postRows.every(post => post.status !== 'published' || (post.number ?? 0) > 0)).toBe(true)
})
it('ist zweimal hintereinander ausführbar', async () => {