feat(media): scaffold mvp1 media and portfolio foundation

This commit is contained in:
2026-02-11 22:46:24 +01:00
parent 5b47fafe89
commit d727ab8b5b
14 changed files with 674 additions and 16 deletions

View File

@@ -1,5 +1,6 @@
import { z } from "zod"
export * from "./media"
export * from "./rbac"
export const postStatusSchema = z.enum(["draft", "published"])

View File

@@ -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<typeof mediaAssetTypeSchema>
export type ArtworkRenditionSlot = z.infer<typeof artworkRenditionSlotSchema>
export type CreateMediaAssetInput = z.infer<typeof createMediaAssetInputSchema>
export type CreateArtworkInput = z.infer<typeof createArtworkInputSchema>

View File

@@ -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;

View File

@@ -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])
}

View File

@@ -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

View File

@@ -1,4 +1,5 @@
export { db } from "./client"
export { getMediaFoundationSummary, listArtworks, listMediaAssets } from "./media-foundation"
export {
createPost,
deletePost,

View File

@@ -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,
}
}

View File

@@ -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"