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';
|
||||
|
||||
import { makeInitialReviewRows } from '../lib/reviews.ts';
|
||||
import { toCardDto } from '../lib/dto.ts';
|
||||
|
||||
import { getDb, type CardsDb } from '../db/connection.ts';
|
||||
import { cards, decks, reviews } from '../db/schema/index.ts';
|
||||
|
|
@ -190,17 +191,3 @@ export function cardsRouter(deps: CardsDeps = {}): Hono<{ Variables: AuthVars }>
|
|||
|
||||
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 { cards, decks } from '../db/schema/index.ts';
|
||||
import { authMiddleware, type AuthVars } from '../middleware/auth.ts';
|
||||
import { toDeckDto } from '../lib/dto.ts';
|
||||
import { ulid } from '../lib/ulid.ts';
|
||||
|
||||
/** Optional injectable DB für Tests. */
|
||||
|
|
@ -162,21 +163,3 @@ export function decksRouter(deps: DecksDeps = {}): Hono<{ Variables: AuthVars }>
|
|||
|
||||
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,
|
||||
publicDecks,
|
||||
} from '../../db/schema/index.ts';
|
||||
import type { AuthorRow } from '../../db/schema/marketplace/index.ts';
|
||||
import { authMiddleware, type AuthVars } from '../../middleware/auth.ts';
|
||||
import { optionalAuthMiddleware } from '../../middleware/marketplace/optional-auth.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 { hashVersionCards } from '../../lib/marketplace/version-hash.ts';
|
||||
|
||||
|
|
@ -104,47 +104,6 @@ function semverGreater(a: string, b: string): boolean {
|
|||
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(
|
||||
deps: MarketplaceDecksDeps = {}
|
||||
): 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);
|
||||
if (!deck) return c.json({ error: 'not_found' }, 404);
|
||||
|
||||
const [versionAndOwner] = await Promise.all([
|
||||
const [latestVersion, ownerRow] = await Promise.all([
|
||||
deck.latestVersionId
|
||||
? db
|
||||
.select()
|
||||
|
|
@ -168,17 +127,13 @@ export function marketplaceDecksRouter(
|
|||
.limit(1)
|
||||
.then((rows) => rows[0] ?? 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({
|
||||
deck: toDeckDto(deck),
|
||||
latest_version: versionAndOwner ? toVersionDto(versionAndOwner) : null,
|
||||
deck: toPublicDeckDto(deck),
|
||||
latest_version: latestVersion ? toVersionDto(latestVersion) : null,
|
||||
owner: ownerRow ? toOwnerDto(ownerRow) : null,
|
||||
});
|
||||
});
|
||||
|
|
@ -250,7 +205,7 @@ export function marketplaceDecksRouter(
|
|||
ownerUserId: userId,
|
||||
})
|
||||
.returning();
|
||||
return c.json(toDeckDto(created), 201);
|
||||
return c.json(toPublicDeckDto(created), 201);
|
||||
});
|
||||
|
||||
// PATCH /:slug — Metadaten.
|
||||
|
|
@ -290,7 +245,7 @@ export function marketplaceDecksRouter(
|
|||
})
|
||||
.where(and(eq(publicDecks.id, deck.id)))
|
||||
.returning();
|
||||
return c.json(toDeckDto(updated));
|
||||
return c.json(toPublicDeckDto(updated));
|
||||
});
|
||||
|
||||
// POST /:slug/publish — neue Version.
|
||||
|
|
@ -389,7 +344,7 @@ export function marketplaceDecksRouter(
|
|||
|
||||
return c.json(
|
||||
{
|
||||
deck: toDeckDto(result.deck),
|
||||
deck: toPublicDeckDto(result.deck),
|
||||
version: toVersionDto(result.version),
|
||||
moderation: {
|
||||
verdict: moderation.verdict,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue