diff --git a/TODO.md b/TODO.md index aad69b8..6fa688d 100644 --- a/TODO.md +++ b/TODO.md @@ -118,7 +118,7 @@ This file is the single source of truth for roadmap and delivery progress. ### MVP1 Suggested Branch Order -- [ ] [P1] `todo/mvp1-media-foundation`: +- [~] [P1] `todo/mvp1-media-foundation`: media model, artwork entity, grouping primitives (gallery/album/category/tag), rendition slots - [ ] [P1] `todo/mvp1-media-upload-pipeline`: S3/local upload adapter, media processing presets, metadata input flows, admin media CRUD UI @@ -267,6 +267,8 @@ This file is the single source of truth for roadmap and delivery progress. - [2026-02-11] Added a staging deployment execution checklist and deployment-record template to capture first real-host rollout evidence. - [2026-02-11] Artist-focused feature map refined: MVP1 covers portfolio media/domain CRUD + announcements + customer/commission linking; MVP2 covers advanced automation (watermark, palette extraction, media transform pipelines). - [2026-02-11] `gaertan` inspiration to reuse: S3 object strategy with signed delivery, commission type/options/extras/custom-input modeling, request-status kanban mapping, and gallery rendition/color extraction patterns. +- [2026-02-11] MVP1 media foundation started: portfolio domain models (`MediaAsset`, `Artwork`, galleries/albums/categories/tags, rendition links) plus initial admin `/media` and `/portfolio` data views. +- [2026-02-11] `prisma migrate dev --name media_foundation` can fail when DB endpoint is unreachable; apply this named migration once `DATABASE_URL` host is reachable again. ## How We Use This File diff --git a/apps/admin/src/app/media/page.tsx b/apps/admin/src/app/media/page.tsx index 56b9eae..fbcf217 100644 --- a/apps/admin/src/app/media/page.tsx +++ b/apps/admin/src/app/media/page.tsx @@ -1,4 +1,5 @@ -import { AdminSectionPlaceholder } from "@/components/admin-section-placeholder" +import { getMediaFoundationSummary, listMediaAssets } from "@cms/db" + import { AdminShell } from "@/components/admin-shell" import { requirePermissionForRoute } from "@/lib/route-guards" @@ -10,6 +11,7 @@ export default async function MediaManagementPage() { permission: "media:read", scope: "team", }) + const [summary, assets] = await Promise.all([getMediaFoundationSummary(), listMediaAssets(20)]) return ( - +
+
+

Media Assets

+

{summary.mediaAssets}

+
+
+

Artworks

+

{summary.artworks}

+
+
+

Groups

+

+ {summary.galleries + summary.albums + summary.categories + summary.tags} +

+

+ {summary.galleries} galleries · {summary.albums} albums · {summary.categories}{" "} + categories{" · "} + {summary.tags} tags +

+
+
+ +
+
+

Recent Media Assets

+ + MVP1 Foundation + +
+
+ + + + + + + + + + + {assets.length === 0 ? ( + + + + ) : ( + assets.map((asset) => ( + + + + + + + )) + )} + +
TitleTypePublishedUpdated
+ No media assets yet. Upload workflows land in `todo/mvp1-media-upload-pipeline`. +
{asset.title}{asset.type}{asset.isPublished ? "yes" : "no"} + {asset.updatedAt.toLocaleDateString("en-US")} +
+
+
) } diff --git a/apps/admin/src/app/portfolio/page.tsx b/apps/admin/src/app/portfolio/page.tsx new file mode 100644 index 0000000..b644b25 --- /dev/null +++ b/apps/admin/src/app/portfolio/page.tsx @@ -0,0 +1,69 @@ +import { listArtworks } from "@cms/db" + +import { AdminShell } from "@/components/admin-shell" +import { requirePermissionForRoute } from "@/lib/route-guards" + +export const dynamic = "force-dynamic" + +export default async function PortfolioPage() { + const role = await requirePermissionForRoute({ + nextPath: "/portfolio", + permission: "media:read", + scope: "team", + }) + const artworks = await listArtworks(30) + + return ( + +
+
+

Artworks

+ + MVP1 Foundation + +
+
+ + + + + + + + + + + + {artworks.length === 0 ? ( + + + + ) : ( + artworks.map((artwork) => ( + + + + + + + + )) + )} + +
TitleSlugPublishedRenditionsGroups
+ No artworks yet. Add creation flows after media upload pipeline lands. +
{artwork.title}{artwork.slug}{artwork.isPublished ? "yes" : "no"}{artwork.renditions.length} + g:{artwork.galleryLinks.length} a:{artwork.albumLinks.length} c: + {artwork.categoryLinks.length} t:{artwork.tagLinks.length} +
+
+
+
+ ) +} diff --git a/apps/admin/src/components/admin-shell.tsx b/apps/admin/src/components/admin-shell.tsx index 73711d9..78d2fe9 100644 --- a/apps/admin/src/components/admin-shell.tsx +++ b/apps/admin/src/components/admin-shell.tsx @@ -27,6 +27,7 @@ const navItems: NavItem[] = [ { href: "/", label: "Dashboard", permission: "dashboard:read", scope: "global" }, { href: "/pages", label: "Pages", permission: "pages:read", scope: "team" }, { href: "/media", label: "Media", permission: "media:read", scope: "team" }, + { href: "/portfolio", label: "Portfolio", permission: "media:read", scope: "team" }, { href: "/users", label: "Users", permission: "users:read", scope: "own" }, { href: "/commissions", label: "Commissions", permission: "commissions:read", scope: "own" }, { href: "/settings", label: "Settings", permission: "users:manage_roles", scope: "global" }, diff --git a/apps/admin/src/lib/access.test.ts b/apps/admin/src/lib/access.test.ts index 6e10102..6c0f156 100644 --- a/apps/admin/src/lib/access.test.ts +++ b/apps/admin/src/lib/access.test.ts @@ -31,6 +31,10 @@ describe("admin route access rules", () => { permission: "media:read", scope: "team", }) + expect(getRequiredPermission("/portfolio")).toEqual({ + permission: "media:read", + scope: "team", + }) expect(getRequiredPermission("/users")).toEqual({ permission: "users:read", scope: "own", diff --git a/apps/admin/src/lib/access.ts b/apps/admin/src/lib/access.ts index ea8aa8c..b8ee02c 100644 --- a/apps/admin/src/lib/access.ts +++ b/apps/admin/src/lib/access.ts @@ -57,6 +57,13 @@ const guardRules: GuardRule[] = [ scope: "team", }, }, + { + route: /^\/portfolio(?:\/|$)/, + requirement: { + permission: "media:read", + scope: "team", + }, + }, { route: /^\/users(?:\/|$)/, requirement: { diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts index 8e55b23..cc099ae 100644 --- a/packages/content/src/index.ts +++ b/packages/content/src/index.ts @@ -1,5 +1,6 @@ import { z } from "zod" +export * from "./media" export * from "./rbac" export const postStatusSchema = z.enum(["draft", "published"]) diff --git a/packages/content/src/media.ts b/packages/content/src/media.ts new file mode 100644 index 0000000..c08c0f2 --- /dev/null +++ b/packages/content/src/media.ts @@ -0,0 +1,39 @@ +import { z } from "zod" + +export const mediaAssetTypeSchema = z.enum([ + "artwork", + "banner", + "promotion", + "video", + "gif", + "generic", +]) + +export const artworkRenditionSlotSchema = z.enum(["thumbnail", "card", "full", "custom"]) + +export const createMediaAssetInputSchema = z.object({ + type: mediaAssetTypeSchema, + title: z.string().min(1).max(180), + description: z.string().max(5000).optional(), + altText: z.string().max(1000).optional(), + source: z.string().max(500).optional(), + copyright: z.string().max(500).optional(), + author: z.string().max(180).optional(), + tags: z.array(z.string().min(1).max(100)).default([]), +}) + +export const createArtworkInputSchema = z.object({ + title: z.string().min(1).max(180), + slug: z.string().min(1).max(180), + description: z.string().max(5000).optional(), + medium: z.string().max(180).optional(), + dimensions: z.string().max(180).optional(), + year: z.number().int().min(1000).max(9999).optional(), + framing: z.string().max(180).optional(), + availability: z.string().max(180).optional(), +}) + +export type MediaAssetType = z.infer +export type ArtworkRenditionSlot = z.infer +export type CreateMediaAssetInput = z.infer +export type CreateArtworkInput = z.infer diff --git a/packages/db/prisma/migrations/20260211214457_media_foundation/migration.sql b/packages/db/prisma/migrations/20260211214457_media_foundation/migration.sql new file mode 100644 index 0000000..df7d210 --- /dev/null +++ b/packages/db/prisma/migrations/20260211214457_media_foundation/migration.sql @@ -0,0 +1,235 @@ +-- CreateTable +CREATE TABLE "MediaAsset" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "altText" TEXT, + "source" TEXT, + "copyright" TEXT, + "author" TEXT, + "tags" TEXT[], + "storageKey" TEXT, + "mimeType" TEXT, + "width" INTEGER, + "height" INTEGER, + "sizeBytes" INTEGER, + "isPublished" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MediaAsset_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Artwork" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + "medium" TEXT, + "dimensions" TEXT, + "year" INTEGER, + "framing" TEXT, + "availability" TEXT, + "isPublished" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Artwork_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ArtworkRendition" ( + "id" TEXT NOT NULL, + "artworkId" TEXT NOT NULL, + "mediaAssetId" TEXT NOT NULL, + "slot" TEXT NOT NULL, + "width" INTEGER, + "height" INTEGER, + "isPrimary" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ArtworkRendition_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Gallery" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "isVisible" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Gallery_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Album" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "isVisible" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Album_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Category" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "isVisible" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Tag" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Tag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ArtworkGallery" ( + "id" TEXT NOT NULL, + "artworkId" TEXT NOT NULL, + "galleryId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ArtworkGallery_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ArtworkAlbum" ( + "id" TEXT NOT NULL, + "artworkId" TEXT NOT NULL, + "albumId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ArtworkAlbum_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ArtworkCategory" ( + "id" TEXT NOT NULL, + "artworkId" TEXT NOT NULL, + "categoryId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ArtworkCategory_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ArtworkTag" ( + "id" TEXT NOT NULL, + "artworkId" TEXT NOT NULL, + "tagId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ArtworkTag_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "MediaAsset_storageKey_key" ON "MediaAsset"("storageKey"); + +-- CreateIndex +CREATE INDEX "MediaAsset_type_idx" ON "MediaAsset"("type"); + +-- CreateIndex +CREATE INDEX "MediaAsset_isPublished_idx" ON "MediaAsset"("isPublished"); + +-- CreateIndex +CREATE UNIQUE INDEX "Artwork_slug_key" ON "Artwork"("slug"); + +-- CreateIndex +CREATE INDEX "Artwork_isPublished_idx" ON "Artwork"("isPublished"); + +-- CreateIndex +CREATE INDEX "ArtworkRendition_mediaAssetId_idx" ON "ArtworkRendition"("mediaAssetId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ArtworkRendition_artworkId_slot_key" ON "ArtworkRendition"("artworkId", "slot"); + +-- CreateIndex +CREATE UNIQUE INDEX "Gallery_slug_key" ON "Gallery"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Album_slug_key" ON "Album"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Category_slug_key" ON "Category"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Tag_slug_key" ON "Tag"("slug"); + +-- CreateIndex +CREATE INDEX "ArtworkGallery_galleryId_idx" ON "ArtworkGallery"("galleryId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ArtworkGallery_artworkId_galleryId_key" ON "ArtworkGallery"("artworkId", "galleryId"); + +-- CreateIndex +CREATE INDEX "ArtworkAlbum_albumId_idx" ON "ArtworkAlbum"("albumId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ArtworkAlbum_artworkId_albumId_key" ON "ArtworkAlbum"("artworkId", "albumId"); + +-- CreateIndex +CREATE INDEX "ArtworkCategory_categoryId_idx" ON "ArtworkCategory"("categoryId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ArtworkCategory_artworkId_categoryId_key" ON "ArtworkCategory"("artworkId", "categoryId"); + +-- CreateIndex +CREATE INDEX "ArtworkTag_tagId_idx" ON "ArtworkTag"("tagId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ArtworkTag_artworkId_tagId_key" ON "ArtworkTag"("artworkId", "tagId"); + +-- AddForeignKey +ALTER TABLE "ArtworkRendition" ADD CONSTRAINT "ArtworkRendition_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtworkRendition" ADD CONSTRAINT "ArtworkRendition_mediaAssetId_fkey" FOREIGN KEY ("mediaAssetId") REFERENCES "MediaAsset"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtworkGallery" ADD CONSTRAINT "ArtworkGallery_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtworkGallery" ADD CONSTRAINT "ArtworkGallery_galleryId_fkey" FOREIGN KEY ("galleryId") REFERENCES "Gallery"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtworkAlbum" ADD CONSTRAINT "ArtworkAlbum_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtworkAlbum" ADD CONSTRAINT "ArtworkAlbum_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtworkCategory" ADD CONSTRAINT "ArtworkCategory_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtworkCategory" ADD CONSTRAINT "ArtworkCategory_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtworkTag" ADD CONSTRAINT "ArtworkTag_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtworkTag" ADD CONSTRAINT "ArtworkTag_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index a135e21..e91258e 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -1,5 +1,6 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client" + output = "./generated/client" } datasource db { @@ -95,3 +96,159 @@ model SystemSetting { @@map("system_setting") } + +model MediaAsset { + id String @id @default(uuid()) + type String + title String + description String? + altText String? + source String? + copyright String? + author String? + tags String[] + storageKey String? @unique + mimeType String? + width Int? + height Int? + sizeBytes Int? + isPublished Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + artworkLinks ArtworkRendition[] + + @@index([type]) + @@index([isPublished]) +} + +model Artwork { + id String @id @default(uuid()) + title String + slug String @unique + description String? + medium String? + dimensions String? + year Int? + framing String? + availability String? + isPublished Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + renditions ArtworkRendition[] + galleryLinks ArtworkGallery[] + albumLinks ArtworkAlbum[] + categoryLinks ArtworkCategory[] + tagLinks ArtworkTag[] + + @@index([isPublished]) +} + +model ArtworkRendition { + id String @id @default(uuid()) + artworkId String + mediaAssetId String + slot String + width Int? + height Int? + isPrimary Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade) + mediaAsset MediaAsset @relation(fields: [mediaAssetId], references: [id], onDelete: Cascade) + + @@unique([artworkId, slot]) + @@index([mediaAssetId]) +} + +model Gallery { + id String @id @default(uuid()) + name String + slug String @unique + description String? + sortOrder Int @default(0) + isVisible Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + artworkLinks ArtworkGallery[] +} + +model Album { + id String @id @default(uuid()) + name String + slug String @unique + description String? + sortOrder Int @default(0) + isVisible Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + artworkLinks ArtworkAlbum[] +} + +model Category { + id String @id @default(uuid()) + name String + slug String @unique + description String? + sortOrder Int @default(0) + isVisible Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + artworkLinks ArtworkCategory[] +} + +model Tag { + id String @id @default(uuid()) + name String + slug String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + artworkLinks ArtworkTag[] +} + +model ArtworkGallery { + id String @id @default(uuid()) + artworkId String + galleryId String + createdAt DateTime @default(now()) + artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade) + gallery Gallery @relation(fields: [galleryId], references: [id], onDelete: Cascade) + + @@unique([artworkId, galleryId]) + @@index([galleryId]) +} + +model ArtworkAlbum { + id String @id @default(uuid()) + artworkId String + albumId String + createdAt DateTime @default(now()) + artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade) + album Album @relation(fields: [albumId], references: [id], onDelete: Cascade) + + @@unique([artworkId, albumId]) + @@index([albumId]) +} + +model ArtworkCategory { + id String @id @default(uuid()) + artworkId String + categoryId String + createdAt DateTime @default(now()) + artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) + + @@unique([artworkId, categoryId]) + @@index([categoryId]) +} + +model ArtworkTag { + id String @id @default(uuid()) + artworkId String + tagId String + createdAt DateTime @default(now()) + artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@unique([artworkId, tagId]) + @@index([tagId]) +} diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index 1efb458..4423065 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -1,6 +1,6 @@ import { PrismaPg } from "@prisma/adapter-pg" -import { PrismaClient } from "@prisma/client" import { Pool } from "pg" +import { PrismaClient } from "../prisma/generated/client/client" const connectionString = process.env.DATABASE_URL diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 8cc6c44..bcbf4be 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,4 +1,5 @@ export { db } from "./client" +export { getMediaFoundationSummary, listArtworks, listMediaAssets } from "./media-foundation" export { createPost, deletePost, diff --git a/packages/db/src/media-foundation.ts b/packages/db/src/media-foundation.ts new file mode 100644 index 0000000..c5620aa --- /dev/null +++ b/packages/db/src/media-foundation.ts @@ -0,0 +1,88 @@ +import { db } from "./client" + +export async function listMediaAssets(limit = 24) { + return db.mediaAsset.findMany({ + orderBy: { updatedAt: "desc" }, + take: limit, + }) +} + +export async function listArtworks(limit = 24) { + return db.artwork.findMany({ + orderBy: { updatedAt: "desc" }, + take: limit, + include: { + renditions: { + select: { + id: true, + slot: true, + mediaAssetId: true, + }, + }, + galleryLinks: { + include: { + gallery: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }, + albumLinks: { + include: { + album: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }, + categoryLinks: { + include: { + category: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }, + tagLinks: { + include: { + tag: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }, + }, + }) +} + +export async function getMediaFoundationSummary() { + const [mediaAssets, artworks, galleries, albums, categories, tags] = await Promise.all([ + db.mediaAsset.count(), + db.artwork.count(), + db.gallery.count(), + db.album.count(), + db.category.count(), + db.tag.count(), + ]) + + return { + mediaAssets, + artworks, + galleries, + albums, + categories, + tags, + } +} diff --git a/packages/db/src/posts.ts b/packages/db/src/posts.ts index e6b9a8b..46051b7 100644 --- a/packages/db/src/posts.ts +++ b/packages/db/src/posts.ts @@ -5,7 +5,7 @@ import { updatePostInputSchema, } from "@cms/content" import { type CrudAuditHook, type CrudMutationContext, createCrudService } from "@cms/crud" -import type { Post } from "@prisma/client" +import type { Post } from "../prisma/generated/client/client" import { db } from "./client"