refactor(api): DTO-Helper extrahieren + N+1 in marketplace/decks beheben
- `lib/dto.ts`: `toDeckDto` und `toCardDto` aus routes/decks.ts und routes/cards.ts extrahiert — testbar, zentrale Output-Shape-Doku - `lib/marketplace/dto.ts`: `toPublicDeckDto`, `toOwnerDto`, `toVersionDto` aus routes/marketplace/decks.ts extrahiert - `GET /:slug` in marketplace/decks.ts: Version + Owner parallel per `Promise.all` statt sequenziell (2 RTT → 1 RTT) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f2f752e9ee
commit
c39bacc971
5 changed files with 87 additions and 86 deletions
33
apps/api/src/lib/dto.ts
Normal file
33
apps/api/src/lib/dto.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { cards, decks } from '../db/schema/index.ts';
|
||||||
|
|
||||||
|
export function toDeckDto(row: typeof decks.$inferSelect) {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
user_id: row.userId,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
color: row.color,
|
||||||
|
category: row.category,
|
||||||
|
visibility: row.visibility,
|
||||||
|
fsrs_settings: row.fsrsSettings,
|
||||||
|
content_hash: row.contentHash,
|
||||||
|
forked_from_marketplace_deck_id: row.forkedFromMarketplaceDeckId,
|
||||||
|
forked_from_marketplace_version_id: row.forkedFromMarketplaceVersionId,
|
||||||
|
created_at: row.createdAt.toISOString(),
|
||||||
|
updated_at: row.updatedAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toCardDto(row: typeof cards.$inferSelect) {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
deck_id: row.deckId,
|
||||||
|
user_id: row.userId,
|
||||||
|
type: row.type,
|
||||||
|
fields: row.fields,
|
||||||
|
media_refs: row.mediaRefs ?? [],
|
||||||
|
content_hash: row.contentHash,
|
||||||
|
created_at: row.createdAt.toISOString(),
|
||||||
|
updated_at: row.updatedAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
43
apps/api/src/lib/marketplace/dto.ts
Normal file
43
apps/api/src/lib/marketplace/dto.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { publicDecks, publicDeckVersions } from '../../db/schema/index.ts';
|
||||||
|
import type { AuthorRow } from '../../db/schema/marketplace/index.ts';
|
||||||
|
|
||||||
|
export function toPublicDeckDto(row: typeof publicDecks.$inferSelect) {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
slug: row.slug,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description,
|
||||||
|
language: row.language,
|
||||||
|
category: row.category,
|
||||||
|
license: row.license,
|
||||||
|
price_credits: row.priceCredits,
|
||||||
|
owner_user_id: row.ownerUserId,
|
||||||
|
latest_version_id: row.latestVersionId,
|
||||||
|
is_featured: row.isFeatured,
|
||||||
|
is_takedown: row.isTakedown,
|
||||||
|
created_at: row.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toOwnerDto(row: AuthorRow) {
|
||||||
|
return {
|
||||||
|
slug: row.slug,
|
||||||
|
display_name: row.displayName,
|
||||||
|
verified_mana: row.verifiedMana,
|
||||||
|
verified_community: row.verifiedCommunity,
|
||||||
|
pseudonym: row.pseudonym,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toVersionDto(row: typeof publicDeckVersions.$inferSelect) {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
deck_id: row.deckId,
|
||||||
|
semver: row.semver,
|
||||||
|
changelog: row.changelog,
|
||||||
|
content_hash: row.contentHash,
|
||||||
|
card_count: row.cardCount,
|
||||||
|
published_at: row.publishedAt.toISOString(),
|
||||||
|
deprecated_at: row.deprecatedAt?.toISOString() ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from '@cards/domain';
|
} from '@cards/domain';
|
||||||
|
|
||||||
import { makeInitialReviewRows } from '../lib/reviews.ts';
|
import { makeInitialReviewRows } from '../lib/reviews.ts';
|
||||||
|
import { toCardDto } from '../lib/dto.ts';
|
||||||
|
|
||||||
import { getDb, type CardsDb } from '../db/connection.ts';
|
import { getDb, type CardsDb } from '../db/connection.ts';
|
||||||
import { cards, decks, reviews } from '../db/schema/index.ts';
|
import { cards, decks, reviews } from '../db/schema/index.ts';
|
||||||
|
|
@ -190,17 +191,3 @@ export function cardsRouter(deps: CardsDeps = {}): Hono<{ Variables: AuthVars }>
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toCardDto(row: typeof cards.$inferSelect) {
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
deck_id: row.deckId,
|
|
||||||
user_id: row.userId,
|
|
||||||
type: row.type,
|
|
||||||
fields: row.fields,
|
|
||||||
media_refs: row.mediaRefs ?? [],
|
|
||||||
content_hash: row.contentHash,
|
|
||||||
created_at: row.createdAt.toISOString(),
|
|
||||||
updated_at: row.updatedAt.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { DeckCreateSchema, DeckUpdateSchema } from '@cards/domain';
|
||||||
import { getDb, type CardsDb } from '../db/connection.ts';
|
import { getDb, type CardsDb } from '../db/connection.ts';
|
||||||
import { cards, decks } from '../db/schema/index.ts';
|
import { cards, decks } from '../db/schema/index.ts';
|
||||||
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
|
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
|
||||||
|
import { toDeckDto } from '../lib/dto.ts';
|
||||||
import { ulid } from '../lib/ulid.ts';
|
import { ulid } from '../lib/ulid.ts';
|
||||||
|
|
||||||
/** Optional injectable DB für Tests. */
|
/** Optional injectable DB für Tests. */
|
||||||
|
|
@ -162,21 +163,3 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDeckDto(row: typeof decks.$inferSelect) {
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
user_id: row.userId,
|
|
||||||
name: row.name,
|
|
||||||
description: row.description,
|
|
||||||
color: row.color,
|
|
||||||
category: row.category,
|
|
||||||
visibility: row.visibility,
|
|
||||||
fsrs_settings: row.fsrsSettings,
|
|
||||||
content_hash: row.contentHash,
|
|
||||||
forked_from_marketplace_deck_id: row.forkedFromMarketplaceDeckId,
|
|
||||||
forked_from_marketplace_version_id: row.forkedFromMarketplaceVersionId,
|
|
||||||
created_at: row.createdAt.toISOString(),
|
|
||||||
updated_at: row.updatedAt.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ import {
|
||||||
publicDeckVersions,
|
publicDeckVersions,
|
||||||
publicDecks,
|
publicDecks,
|
||||||
} from '../../db/schema/index.ts';
|
} from '../../db/schema/index.ts';
|
||||||
import type { AuthorRow } from '../../db/schema/marketplace/index.ts';
|
|
||||||
import { authMiddleware, type AuthVars } from '../../middleware/auth.ts';
|
import { authMiddleware, type AuthVars } from '../../middleware/auth.ts';
|
||||||
import { optionalAuthMiddleware } from '../../middleware/marketplace/optional-auth.ts';
|
import { optionalAuthMiddleware } from '../../middleware/marketplace/optional-auth.ts';
|
||||||
import { moderateDeckContent } from '../../lib/marketplace/ai-moderation.ts';
|
import { moderateDeckContent } from '../../lib/marketplace/ai-moderation.ts';
|
||||||
|
import { toPublicDeckDto, toOwnerDto, toVersionDto } from '../../lib/marketplace/dto.ts';
|
||||||
import { validateSlug } from '../../lib/marketplace/slug.ts';
|
import { validateSlug } from '../../lib/marketplace/slug.ts';
|
||||||
import { hashVersionCards } from '../../lib/marketplace/version-hash.ts';
|
import { hashVersionCards } from '../../lib/marketplace/version-hash.ts';
|
||||||
|
|
||||||
|
|
@ -104,47 +104,6 @@ function semverGreater(a: string, b: string): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDeckDto(row: typeof publicDecks.$inferSelect) {
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
slug: row.slug,
|
|
||||||
title: row.title,
|
|
||||||
description: row.description,
|
|
||||||
language: row.language,
|
|
||||||
category: row.category,
|
|
||||||
license: row.license,
|
|
||||||
price_credits: row.priceCredits,
|
|
||||||
owner_user_id: row.ownerUserId,
|
|
||||||
latest_version_id: row.latestVersionId,
|
|
||||||
is_featured: row.isFeatured,
|
|
||||||
is_takedown: row.isTakedown,
|
|
||||||
created_at: row.createdAt.toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function toOwnerDto(row: AuthorRow) {
|
|
||||||
return {
|
|
||||||
slug: row.slug,
|
|
||||||
display_name: row.displayName,
|
|
||||||
verified_mana: row.verifiedMana,
|
|
||||||
verified_community: row.verifiedCommunity,
|
|
||||||
pseudonym: row.pseudonym,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function toVersionDto(row: typeof publicDeckVersions.$inferSelect) {
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
deck_id: row.deckId,
|
|
||||||
semver: row.semver,
|
|
||||||
changelog: row.changelog,
|
|
||||||
content_hash: row.contentHash,
|
|
||||||
card_count: row.cardCount,
|
|
||||||
published_at: row.publishedAt.toISOString(),
|
|
||||||
deprecated_at: row.deprecatedAt?.toISOString() ?? null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function marketplaceDecksRouter(
|
export function marketplaceDecksRouter(
|
||||||
deps: MarketplaceDecksDeps = {}
|
deps: MarketplaceDecksDeps = {}
|
||||||
): Hono<{ Variables: Partial<AuthVars> }> {
|
): Hono<{ Variables: Partial<AuthVars> }> {
|
||||||
|
|
@ -159,7 +118,7 @@ export function marketplaceDecksRouter(
|
||||||
const [deck] = await db.select().from(publicDecks).where(eq(publicDecks.slug, slug)).limit(1);
|
const [deck] = await db.select().from(publicDecks).where(eq(publicDecks.slug, slug)).limit(1);
|
||||||
if (!deck) return c.json({ error: 'not_found' }, 404);
|
if (!deck) return c.json({ error: 'not_found' }, 404);
|
||||||
|
|
||||||
const [versionAndOwner] = await Promise.all([
|
const [latestVersion, ownerRow] = await Promise.all([
|
||||||
deck.latestVersionId
|
deck.latestVersionId
|
||||||
? db
|
? db
|
||||||
.select()
|
.select()
|
||||||
|
|
@ -168,17 +127,13 @@ export function marketplaceDecksRouter(
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.then((rows) => rows[0] ?? null)
|
.then((rows) => rows[0] ?? null)
|
||||||
: Promise.resolve(null),
|
: Promise.resolve(null),
|
||||||
|
db.select().from(authors).where(eq(authors.userId, deck.ownerUserId)).limit(1)
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [ownerRow] = await db
|
|
||||||
.select()
|
|
||||||
.from(authors)
|
|
||||||
.where(eq(authors.userId, deck.ownerUserId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
deck: toDeckDto(deck),
|
deck: toPublicDeckDto(deck),
|
||||||
latest_version: versionAndOwner ? toVersionDto(versionAndOwner) : null,
|
latest_version: latestVersion ? toVersionDto(latestVersion) : null,
|
||||||
owner: ownerRow ? toOwnerDto(ownerRow) : null,
|
owner: ownerRow ? toOwnerDto(ownerRow) : null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -250,7 +205,7 @@ export function marketplaceDecksRouter(
|
||||||
ownerUserId: userId,
|
ownerUserId: userId,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
return c.json(toDeckDto(created), 201);
|
return c.json(toPublicDeckDto(created), 201);
|
||||||
});
|
});
|
||||||
|
|
||||||
// PATCH /:slug — Metadaten.
|
// PATCH /:slug — Metadaten.
|
||||||
|
|
@ -290,7 +245,7 @@ export function marketplaceDecksRouter(
|
||||||
})
|
})
|
||||||
.where(and(eq(publicDecks.id, deck.id)))
|
.where(and(eq(publicDecks.id, deck.id)))
|
||||||
.returning();
|
.returning();
|
||||||
return c.json(toDeckDto(updated));
|
return c.json(toPublicDeckDto(updated));
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /:slug/publish — neue Version.
|
// POST /:slug/publish — neue Version.
|
||||||
|
|
@ -389,7 +344,7 @@ export function marketplaceDecksRouter(
|
||||||
|
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
deck: toDeckDto(result.deck),
|
deck: toPublicDeckDto(result.deck),
|
||||||
version: toVersionDto(result.version),
|
version: toVersionDto(result.version),
|
||||||
moderation: {
|
moderation: {
|
||||||
verdict: moderation.verdict,
|
verdict: moderation.verdict,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue