fix: replace any types in API routes with proper Prisma types

- add ContentNode, GalleryItem, TextNode, ParagraphNode, DocContent types
- use Prisma.JsonValue for JSON column content
- use Prisma.ProjectUpdateInput and Prisma.PostUpdateInput for update payloads
- improve type guards for content filtering
- replace any[] with never[] for empty placeholder arrays
This commit is contained in:
Justin Edmund 2025-11-23 05:04:04 -08:00
parent 056e8927ee
commit aab78f3909
2 changed files with 73 additions and 30 deletions

View file

@ -1,4 +1,5 @@
import type { RequestHandler } from './$types' import type { RequestHandler } from './$types'
import type { Prisma } from '@prisma/client'
import { prisma } from '$lib/server/database' import { prisma } from '$lib/server/database'
import { import {
jsonResponse, jsonResponse,
@ -11,6 +12,20 @@ import { removeMediaUsage, extractMediaIds } from '$lib/server/media-usage.js'
import { deleteFile, extractPublicId, isCloudinaryConfigured } from '$lib/server/cloudinary' import { deleteFile, extractPublicId, isCloudinaryConfigured } from '$lib/server/cloudinary'
import { deleteFileLocally } from '$lib/server/local-storage' import { deleteFileLocally } from '$lib/server/local-storage'
// Type for content node structure
interface ContentNode {
type: string
attrs?: Record<string, unknown>
content?: ContentNode[]
}
// Type for gallery item in JSON fields
interface GalleryItem {
id?: number
mediaId?: number
[key: string]: unknown
}
// DELETE /api/media/bulk-delete - Delete multiple media files and clean up references // DELETE /api/media/bulk-delete - Delete multiple media files and clean up references
export const DELETE: RequestHandler = async (event) => { export const DELETE: RequestHandler = async (event) => {
// Check authentication // Check authentication
@ -165,7 +180,7 @@ async function cleanupMediaReferences(mediaIds: number[]) {
for (const project of projects) { for (const project of projects) {
let needsUpdate = false let needsUpdate = false
const updateData: any = {} const updateData: Prisma.ProjectUpdateInput = {}
// Check featured image // Check featured image
if (project.featuredImage && urlsToRemove.includes(project.featuredImage)) { if (project.featuredImage && urlsToRemove.includes(project.featuredImage)) {
@ -181,9 +196,9 @@ async function cleanupMediaReferences(mediaIds: number[]) {
// Check gallery // Check gallery
if (project.gallery && Array.isArray(project.gallery)) { if (project.gallery && Array.isArray(project.gallery)) {
const filteredGallery = project.gallery.filter((item: any) => { const filteredGallery = (project.gallery as GalleryItem[]).filter((item) => {
const itemId = typeof item === 'object' ? item.id : parseInt(item) const itemId = item.id || item.mediaId
return !mediaIds.includes(itemId) return itemId ? !mediaIds.includes(Number(itemId)) : true
}) })
if (filteredGallery.length !== project.gallery.length) { if (filteredGallery.length !== project.gallery.length) {
updateData.gallery = filteredGallery.length > 0 ? filteredGallery : null updateData.gallery = filteredGallery.length > 0 ? filteredGallery : null
@ -221,7 +236,7 @@ async function cleanupMediaReferences(mediaIds: number[]) {
for (const post of posts) { for (const post of posts) {
let needsUpdate = false let needsUpdate = false
const updateData: any = {} const updateData: Prisma.PostUpdateInput = {}
// Check featured image // Check featured image
if (post.featuredImage && urlsToRemove.includes(post.featuredImage)) { if (post.featuredImage && urlsToRemove.includes(post.featuredImage)) {
@ -231,9 +246,9 @@ async function cleanupMediaReferences(mediaIds: number[]) {
// Check attachments // Check attachments
if (post.attachments && Array.isArray(post.attachments)) { if (post.attachments && Array.isArray(post.attachments)) {
const filteredAttachments = post.attachments.filter((item: any) => { const filteredAttachments = (post.attachments as GalleryItem[]).filter((item) => {
const itemId = typeof item === 'object' ? item.id : parseInt(item) const itemId = item.id || item.mediaId
return !mediaIds.includes(itemId) return itemId ? !mediaIds.includes(Number(itemId)) : true
}) })
if (filteredAttachments.length !== post.attachments.length) { if (filteredAttachments.length !== post.attachments.length) {
updateData.attachments = filteredAttachments.length > 0 ? filteredAttachments : null updateData.attachments = filteredAttachments.length > 0 ? filteredAttachments : null
@ -263,27 +278,30 @@ async function cleanupMediaReferences(mediaIds: number[]) {
/** /**
* Remove media references from rich text content * Remove media references from rich text content
*/ */
function cleanContentFromMedia(content: any, mediaIds: number[], urlsToRemove: string[]): any { function cleanContentFromMedia(content: Prisma.JsonValue, mediaIds: number[], urlsToRemove: string[]): Prisma.JsonValue {
if (!content || typeof content !== 'object') return content if (!content || typeof content !== 'object') return content
function cleanNode(node: any): any { function cleanNode(node: ContentNode | null): ContentNode | null {
if (!node) return node if (!node) return node
// Remove image nodes that reference deleted media // Remove image nodes that reference deleted media
if (node.type === 'image' && node.attrs?.src) { if (node.type === 'image' && node.attrs?.src) {
const shouldRemove = urlsToRemove.some((url) => node.attrs.src.includes(url)) const shouldRemove = urlsToRemove.some((url) => String(node.attrs?.src).includes(url))
if (shouldRemove) { if (shouldRemove) {
return null // Mark for removal return null // Mark for removal
} }
} }
// Clean gallery nodes // Clean gallery nodes
if (node.type === 'gallery' && node.attrs?.images) { if (node.type === 'gallery' && node.attrs?.images && Array.isArray(node.attrs.images)) {
const filteredImages = node.attrs.images.filter((image: any) => !mediaIds.includes(image.id)) const filteredImages = (node.attrs.images as GalleryItem[]).filter((image) => {
const imageId = image.id || image.mediaId
return imageId ? !mediaIds.includes(Number(imageId)) : true
})
if (filteredImages.length === 0) { if (filteredImages.length === 0) {
return null // Remove empty gallery return null // Remove empty gallery
} else if (filteredImages.length !== node.attrs.images.length) { } else if (filteredImages.length !== (node.attrs.images as unknown[]).length) {
return { return {
...node, ...node,
attrs: { attrs: {
@ -296,7 +314,7 @@ function cleanContentFromMedia(content: any, mediaIds: number[], urlsToRemove: s
// Recursively clean child nodes // Recursively clean child nodes
if (node.content) { if (node.content) {
const cleanedContent = node.content.map(cleanNode).filter((child: any) => child !== null) const cleanedContent = node.content.map(cleanNode).filter((child): child is ContentNode => child !== null)
return { return {
...node, ...node,

View file

@ -1,8 +1,32 @@
import type { RequestHandler } from './$types' import type { RequestHandler } from './$types'
import type { Prisma } from '@prisma/client'
import { prisma } from '$lib/server/database' import { prisma } from '$lib/server/database'
import { logger } from '$lib/server/logger' import { logger } from '$lib/server/logger'
import { renderEdraContent } from '$lib/utils/content' import { renderEdraContent } from '$lib/utils/content'
// Content node types for TipTap/Edra content
interface TextNode {
type: 'text'
text: string
marks?: unknown[]
}
interface ParagraphNode {
type: 'paragraph'
content?: (TextNode | ContentNode)[]
}
interface ContentNode {
type: string
content?: ContentNode[]
attrs?: Record<string, unknown>
}
interface DocContent {
type: 'doc'
content?: ContentNode[]
}
// Helper function to escape XML special characters // Helper function to escape XML special characters
function escapeXML(str: string): string { function escapeXML(str: string): string {
if (!str) return '' if (!str) return ''
@ -16,7 +40,7 @@ function escapeXML(str: string): string {
// Helper function to convert content to HTML for full content // Helper function to convert content to HTML for full content
// Uses the same rendering logic as the website for consistency // Uses the same rendering logic as the website for consistency
function convertContentToHTML(content: any): string { function convertContentToHTML(content: Prisma.JsonValue): string {
if (!content) return '' if (!content) return ''
// Handle legacy content format (if it's just a string) // Handle legacy content format (if it's just a string)
@ -30,7 +54,7 @@ function convertContentToHTML(content: any): string {
} }
// Helper function to extract text summary from content // Helper function to extract text summary from content
function extractTextSummary(content: any, maxLength: number = 300): string { function extractTextSummary(content: Prisma.JsonValue, maxLength: number = 300): string {
if (!content) return '' if (!content) return ''
let text = '' let text = ''
@ -40,20 +64,21 @@ function extractTextSummary(content: any, maxLength: number = 300): string {
text = content text = content
} }
// Handle TipTap/Edra format // Handle TipTap/Edra format
else if (content.type === 'doc' && content.content && Array.isArray(content.content)) { else if (typeof content === 'object' && content !== null && 'type' in content && content.type === 'doc' && 'content' in content && Array.isArray(content.content)) {
text = content.content const docContent = content as DocContent
.filter((node: any) => node.type === 'paragraph') text = docContent.content
.map((node: any) => { ?.filter((node): node is ParagraphNode => node.type === 'paragraph')
.map((node) => {
if (node.content && Array.isArray(node.content)) { if (node.content && Array.isArray(node.content)) {
return node.content return node.content
.filter((child: any) => child.type === 'text') .filter((child): child is TextNode => 'type' in child && child.type === 'text')
.map((child: any) => child.text || '') .map((child) => child.text || '')
.join('') .join('')
} }
return '' return ''
}) })
.filter((t: string) => t) .filter((t) => t.length > 0)
.join(' ') .join(' ') || ''
} }
// Clean up and truncate // Clean up and truncate
@ -79,8 +104,8 @@ export const GET: RequestHandler = async (event) => {
}) })
// TODO: Re-enable albums once database schema is updated // TODO: Re-enable albums once database schema is updated
const universeAlbums: any[] = [] const universeAlbums: never[] = []
const photoAlbums: any[] = [] const photoAlbums: never[] = []
// Combine all content types // Combine all content types
const items = [ const items = [