diff --git a/apps/api/src/lib/dto.ts b/apps/api/src/lib/dto.ts new file mode 100644 index 0000000..6574951 --- /dev/null +++ b/apps/api/src/lib/dto.ts @@ -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(), + }; +} diff --git a/apps/api/src/lib/marketplace/dto.ts b/apps/api/src/lib/marketplace/dto.ts new file mode 100644 index 0000000..55c1ebf --- /dev/null +++ b/apps/api/src/lib/marketplace/dto.ts @@ -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, + }; +} diff --git a/apps/api/src/routes/cards.ts b/apps/api/src/routes/cards.ts index a175254..cff6ddc 100644 --- a/apps/api/src/routes/cards.ts +++ b/apps/api/src/routes/cards.ts @@ -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(), - }; -} diff --git a/apps/api/src/routes/decks.ts b/apps/api/src/routes/decks.ts index 2cee34a..eaa9f13 100644 --- a/apps/api/src/routes/decks.ts +++ b/apps/api/src/routes/decks.ts @@ -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(), - }; -} diff --git a/apps/api/src/routes/marketplace/decks.ts b/apps/api/src/routes/marketplace/decks.ts index 70b1331..98a64b5 100644 --- a/apps/api/src/routes/marketplace/decks.ts +++ b/apps/api/src/routes/marketplace/decks.ts @@ -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 }> { @@ -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,