feat(media): complete mvp1 media foundation workflows
This commit is contained in:
51
packages/content/src/media.test.ts
Normal file
51
packages/content/src/media.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import {
|
||||
attachArtworkRenditionInputSchema,
|
||||
createGroupingInputSchema,
|
||||
createMediaAssetInputSchema,
|
||||
linkArtworkGroupingInputSchema,
|
||||
} from "./media"
|
||||
|
||||
describe("media schemas", () => {
|
||||
it("accepts supported media asset type payload", () => {
|
||||
const parsed = createMediaAssetInputSchema.parse({
|
||||
type: "artwork",
|
||||
title: "Artwork",
|
||||
tags: ["tag-a"],
|
||||
})
|
||||
|
||||
expect(parsed.type).toBe("artwork")
|
||||
expect(parsed.tags).toEqual(["tag-a"])
|
||||
})
|
||||
|
||||
it("validates grouping link payload", () => {
|
||||
const parsed = linkArtworkGroupingInputSchema.parse({
|
||||
artworkId: "f40f4bcc-7148-45d7-a19d-856f7146a47e",
|
||||
groupType: "gallery",
|
||||
groupId: "f4e094df-0edf-4d5a-8b7b-c51f09cae95e",
|
||||
})
|
||||
|
||||
expect(parsed.groupType).toBe("gallery")
|
||||
})
|
||||
|
||||
it("enforces rendition slot enum", () => {
|
||||
const parsed = attachArtworkRenditionInputSchema.parse({
|
||||
artworkId: "f40f4bcc-7148-45d7-a19d-856f7146a47e",
|
||||
mediaAssetId: "f4e094df-0edf-4d5a-8b7b-c51f09cae95e",
|
||||
slot: "thumbnail",
|
||||
})
|
||||
|
||||
expect(parsed.slot).toBe("thumbnail")
|
||||
})
|
||||
|
||||
it("supports grouping defaults", () => {
|
||||
const parsed = createGroupingInputSchema.parse({
|
||||
name: "Featured",
|
||||
slug: "featured",
|
||||
})
|
||||
|
||||
expect(parsed.sortOrder).toBe(0)
|
||||
expect(parsed.isVisible).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -33,7 +33,33 @@ export const createArtworkInputSchema = z.object({
|
||||
availability: z.string().max(180).optional(),
|
||||
})
|
||||
|
||||
export const createGroupingInputSchema = z.object({
|
||||
name: z.string().min(1).max(180),
|
||||
slug: z.string().min(1).max(180),
|
||||
description: z.string().max(5000).optional(),
|
||||
sortOrder: z.number().int().min(0).default(0),
|
||||
isVisible: z.boolean().default(true),
|
||||
})
|
||||
|
||||
export const linkArtworkGroupingInputSchema = z.object({
|
||||
artworkId: z.string().uuid(),
|
||||
groupType: z.enum(["gallery", "album", "category", "tag"]),
|
||||
groupId: z.string().uuid(),
|
||||
})
|
||||
|
||||
export const attachArtworkRenditionInputSchema = z.object({
|
||||
artworkId: z.string().uuid(),
|
||||
mediaAssetId: z.string().uuid(),
|
||||
slot: artworkRenditionSlotSchema,
|
||||
width: z.number().int().positive().optional(),
|
||||
height: z.number().int().positive().optional(),
|
||||
isPrimary: z.boolean().default(false),
|
||||
})
|
||||
|
||||
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>
|
||||
export type CreateGroupingInput = z.infer<typeof createGroupingInputSchema>
|
||||
export type LinkArtworkGroupingInput = z.infer<typeof linkArtworkGroupingInputSchema>
|
||||
export type AttachArtworkRenditionInput = z.infer<typeof attachArtworkRenditionInputSchema>
|
||||
|
||||
@@ -13,6 +13,75 @@ async function main() {
|
||||
},
|
||||
})
|
||||
|
||||
const media = await db.mediaAsset.upsert({
|
||||
where: { storageKey: "seed/artwork-welcome.jpg" },
|
||||
update: {},
|
||||
create: {
|
||||
type: "artwork",
|
||||
title: "Seed Artwork Image",
|
||||
altText: "Seed artwork placeholder",
|
||||
tags: ["seed", "portfolio"],
|
||||
storageKey: "seed/artwork-welcome.jpg",
|
||||
mimeType: "image/jpeg",
|
||||
isPublished: true,
|
||||
},
|
||||
})
|
||||
|
||||
const artwork = await db.artwork.upsert({
|
||||
where: { slug: "seed-artwork-welcome" },
|
||||
update: {},
|
||||
create: {
|
||||
title: "Seed Artwork",
|
||||
slug: "seed-artwork-welcome",
|
||||
description: "Baseline seeded artwork for MVP1 media foundation.",
|
||||
medium: "Digital",
|
||||
year: 2026,
|
||||
availability: "available",
|
||||
isPublished: true,
|
||||
},
|
||||
})
|
||||
|
||||
const gallery = await db.gallery.upsert({
|
||||
where: { slug: "featured" },
|
||||
update: {},
|
||||
create: {
|
||||
name: "Featured",
|
||||
slug: "featured",
|
||||
description: "Featured artwork selection.",
|
||||
isVisible: true,
|
||||
},
|
||||
})
|
||||
|
||||
await db.artworkGallery.upsert({
|
||||
where: {
|
||||
artworkId_galleryId: {
|
||||
artworkId: artwork.id,
|
||||
galleryId: gallery.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
artworkId: artwork.id,
|
||||
galleryId: gallery.id,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
|
||||
await db.artworkRendition.upsert({
|
||||
where: {
|
||||
artworkId_slot: {
|
||||
artworkId: artwork.id,
|
||||
slot: "thumbnail",
|
||||
},
|
||||
},
|
||||
create: {
|
||||
artworkId: artwork.id,
|
||||
mediaAssetId: media.id,
|
||||
slot: "thumbnail",
|
||||
isPrimary: true,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
|
||||
await db.systemSetting.upsert({
|
||||
where: { key: "public.header_banner" },
|
||||
update: {},
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export { db } from "./client"
|
||||
export { getMediaFoundationSummary, listArtworks, listMediaAssets } from "./media-foundation"
|
||||
export {
|
||||
attachArtworkRendition,
|
||||
createAlbum,
|
||||
createArtwork,
|
||||
createCategory,
|
||||
createGallery,
|
||||
createMediaAsset,
|
||||
createTag,
|
||||
getMediaFoundationSummary,
|
||||
linkArtworkToGrouping,
|
||||
listArtworks,
|
||||
listMediaAssets,
|
||||
listMediaFoundationGroups,
|
||||
} from "./media-foundation"
|
||||
export {
|
||||
createPost,
|
||||
deletePost,
|
||||
|
||||
93
packages/db/src/media-foundation.test.ts
Normal file
93
packages/db/src/media-foundation.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const { mockDb } = vi.hoisted(() => ({
|
||||
mockDb: {
|
||||
artworkGallery: { upsert: vi.fn() },
|
||||
artworkAlbum: { upsert: vi.fn() },
|
||||
artworkCategory: { upsert: vi.fn() },
|
||||
artworkTag: { upsert: vi.fn() },
|
||||
artworkRendition: { upsert: vi.fn() },
|
||||
mediaAsset: { create: vi.fn() },
|
||||
artwork: { create: vi.fn() },
|
||||
gallery: { create: vi.fn() },
|
||||
album: { create: vi.fn() },
|
||||
category: { create: vi.fn() },
|
||||
tag: { create: vi.fn() },
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
db: mockDb,
|
||||
}))
|
||||
|
||||
import {
|
||||
attachArtworkRendition,
|
||||
createArtwork,
|
||||
createMediaAsset,
|
||||
linkArtworkToGrouping,
|
||||
} from "./media-foundation"
|
||||
|
||||
describe("media foundation service", () => {
|
||||
beforeEach(() => {
|
||||
for (const value of Object.values(mockDb)) {
|
||||
if ("upsert" in value) {
|
||||
value.upsert.mockReset()
|
||||
}
|
||||
if ("create" in value) {
|
||||
value.create.mockReset()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("routes grouping links to the correct link table", async () => {
|
||||
mockDb.artworkAlbum.upsert.mockResolvedValue({ id: "link" })
|
||||
|
||||
await linkArtworkToGrouping({
|
||||
artworkId: "f40f4bcc-7148-45d7-a19d-856f7146a47e",
|
||||
groupType: "album",
|
||||
groupId: "f4e094df-0edf-4d5a-8b7b-c51f09cae95e",
|
||||
})
|
||||
|
||||
expect(mockDb.artworkAlbum.upsert).toHaveBeenCalledTimes(1)
|
||||
expect(mockDb.artworkGallery.upsert).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("upserts rendition by artwork and slot", async () => {
|
||||
mockDb.artworkRendition.upsert.mockResolvedValue({ id: "rendition" })
|
||||
|
||||
await attachArtworkRendition({
|
||||
artworkId: "f40f4bcc-7148-45d7-a19d-856f7146a47e",
|
||||
mediaAssetId: "f4e094df-0edf-4d5a-8b7b-c51f09cae95e",
|
||||
slot: "thumbnail",
|
||||
isPrimary: true,
|
||||
})
|
||||
|
||||
expect(mockDb.artworkRendition.upsert).toHaveBeenCalledTimes(1)
|
||||
expect(mockDb.artworkRendition.upsert.mock.calls[0]?.[0]).toMatchObject({
|
||||
where: {
|
||||
artworkId_slot: {
|
||||
artworkId: "f40f4bcc-7148-45d7-a19d-856f7146a47e",
|
||||
slot: "thumbnail",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("parses and forwards media and artwork creation payloads", async () => {
|
||||
mockDb.mediaAsset.create.mockResolvedValue({ id: "asset" })
|
||||
mockDb.artwork.create.mockResolvedValue({ id: "artwork" })
|
||||
|
||||
await createMediaAsset({
|
||||
type: "generic",
|
||||
title: "Asset",
|
||||
tags: [],
|
||||
})
|
||||
await createArtwork({
|
||||
title: "Artwork",
|
||||
slug: "artwork",
|
||||
})
|
||||
|
||||
expect(mockDb.mediaAsset.create).toHaveBeenCalledTimes(1)
|
||||
expect(mockDb.artwork.create).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,11 @@
|
||||
import {
|
||||
attachArtworkRenditionInputSchema,
|
||||
createArtworkInputSchema,
|
||||
createGroupingInputSchema,
|
||||
createMediaAssetInputSchema,
|
||||
linkArtworkGroupingInputSchema,
|
||||
} from "@cms/content"
|
||||
|
||||
import { db } from "./client"
|
||||
|
||||
export async function listMediaAssets(limit = 24) {
|
||||
@@ -67,6 +75,169 @@ export async function listArtworks(limit = 24) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function listMediaFoundationGroups() {
|
||||
const [galleries, albums, categories, tags] = await Promise.all([
|
||||
db.gallery.findMany({
|
||||
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
||||
}),
|
||||
db.album.findMany({
|
||||
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
||||
}),
|
||||
db.category.findMany({
|
||||
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
||||
}),
|
||||
db.tag.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
}),
|
||||
])
|
||||
|
||||
return {
|
||||
galleries,
|
||||
albums,
|
||||
categories,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
|
||||
export async function createMediaAsset(input: unknown) {
|
||||
const payload = createMediaAssetInputSchema.parse(input)
|
||||
|
||||
return db.mediaAsset.create({
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createArtwork(input: unknown) {
|
||||
const payload = createArtworkInputSchema.parse(input)
|
||||
|
||||
return db.artwork.create({
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createGallery(input: unknown) {
|
||||
const payload = createGroupingInputSchema.parse(input)
|
||||
|
||||
return db.gallery.create({
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createAlbum(input: unknown) {
|
||||
const payload = createGroupingInputSchema.parse(input)
|
||||
|
||||
return db.album.create({
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createCategory(input: unknown) {
|
||||
const payload = createGroupingInputSchema.parse(input)
|
||||
|
||||
return db.category.create({
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createTag(input: unknown) {
|
||||
const payload = createGroupingInputSchema
|
||||
.pick({
|
||||
name: true,
|
||||
slug: true,
|
||||
})
|
||||
.parse(input)
|
||||
|
||||
return db.tag.create({
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function linkArtworkToGrouping(input: unknown) {
|
||||
const payload = linkArtworkGroupingInputSchema.parse(input)
|
||||
|
||||
if (payload.groupType === "gallery") {
|
||||
return db.artworkGallery.upsert({
|
||||
where: {
|
||||
artworkId_galleryId: {
|
||||
artworkId: payload.artworkId,
|
||||
galleryId: payload.groupId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
artworkId: payload.artworkId,
|
||||
galleryId: payload.groupId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
}
|
||||
|
||||
if (payload.groupType === "album") {
|
||||
return db.artworkAlbum.upsert({
|
||||
where: {
|
||||
artworkId_albumId: {
|
||||
artworkId: payload.artworkId,
|
||||
albumId: payload.groupId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
artworkId: payload.artworkId,
|
||||
albumId: payload.groupId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
}
|
||||
|
||||
if (payload.groupType === "category") {
|
||||
return db.artworkCategory.upsert({
|
||||
where: {
|
||||
artworkId_categoryId: {
|
||||
artworkId: payload.artworkId,
|
||||
categoryId: payload.groupId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
artworkId: payload.artworkId,
|
||||
categoryId: payload.groupId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
}
|
||||
|
||||
return db.artworkTag.upsert({
|
||||
where: {
|
||||
artworkId_tagId: {
|
||||
artworkId: payload.artworkId,
|
||||
tagId: payload.groupId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
artworkId: payload.artworkId,
|
||||
tagId: payload.groupId,
|
||||
},
|
||||
update: {},
|
||||
})
|
||||
}
|
||||
|
||||
export async function attachArtworkRendition(input: unknown) {
|
||||
const payload = attachArtworkRenditionInputSchema.parse(input)
|
||||
|
||||
return db.artworkRendition.upsert({
|
||||
where: {
|
||||
artworkId_slot: {
|
||||
artworkId: payload.artworkId,
|
||||
slot: payload.slot,
|
||||
},
|
||||
},
|
||||
create: payload,
|
||||
update: {
|
||||
mediaAssetId: payload.mediaAssetId,
|
||||
width: payload.width,
|
||||
height: payload.height,
|
||||
isPrimary: payload.isPrimary,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function getMediaFoundationSummary() {
|
||||
const [mediaAssets, artworks, galleries, albums, categories, tags] = await Promise.all([
|
||||
db.mediaAsset.count(),
|
||||
|
||||
Reference in New Issue
Block a user