fix: resolve all TypeScript errors in production code

- Add Where type imports and proper type assertions in API routes
- Add Locale type definitions for locale validation
- Fix email-logs/stats route with proper EmailLog typing
- Fix newsletter-service interests type and null checks
- Remove invalid contact field from OpenAPI metadata
- Fix formSubmissionOverrides type casting in payload.config
- Fix vcard route Team type casting

All 24 TypeScript errors in src/ are now resolved.
Test files have separate type issues that don't affect production.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2025-12-15 09:08:16 +00:00
parent 2faefdac1e
commit 9016d3c06c
9 changed files with 54 additions and 38 deletions

View file

@ -3,8 +3,11 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config'
import type { Category, Media, Post } from '@/payload-types'
type Locale = 'de' | 'en' | 'all'
import {
searchLimiter,
rateLimitHeaders,
@ -49,8 +52,8 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
const includeRelated = searchParams.get('includeRelated') !== 'false' // Default true
// Validate locale
const validLocales = ['de', 'en']
const locale = localeParam && validLocales.includes(localeParam) ? localeParam : 'de'
const validLocales: Locale[] = ['de', 'en']
const locale: Locale = localeParam && validLocales.includes(localeParam as Locale) ? (localeParam as Locale) : 'de'
// Parse tenant ID - REQUIRED for tenant isolation
const tenantId = tenantParam ? parseInt(tenantParam, 10) : undefined
@ -68,7 +71,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
const payload = await getPayload({ config })
// Build where clause (tenant is now required)
const where: Record<string, unknown> = {
const where: Where = {
slug: { equals: slug },
status: { equals: 'published' },
tenant: { equals: tenantId },

View file

@ -3,8 +3,11 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config'
import type { Category, Media, Post } from '@/payload-types'
type Locale = 'de' | 'en' | 'all'
import {
searchLimiter,
rateLimitHeaders,
@ -29,7 +32,7 @@ interface NewsQueryParams {
search?: string
year?: number
month?: number
locale: string
locale: Locale
page: number
limit: number
excludeIds?: number[]
@ -51,7 +54,7 @@ async function getNews(payload: Awaited<ReturnType<typeof getPayload>>, params:
} = params
// Build where clause
const where: Record<string, unknown> = {
const where: Where = {
status: { equals: 'published' },
}
@ -131,7 +134,7 @@ async function getNews(payload: Awaited<ReturnType<typeof getPayload>>, params:
// Execute query
return payload.find({
collection: 'posts',
where,
where: where as Where,
sort: '-publishedAt',
page,
limit,
@ -144,16 +147,16 @@ async function getNews(payload: Awaited<ReturnType<typeof getPayload>>, params:
async function getCategories(
payload: Awaited<ReturnType<typeof getPayload>>,
tenantId?: number,
locale: string = 'de'
locale: Locale = 'de'
) {
const where: Record<string, unknown> = {}
const where: Where = {}
if (tenantId) {
where.tenant = { equals: tenantId }
}
const result = await payload.find({
collection: 'categories',
where,
where: where as Where,
sort: 'name',
limit: 100,
locale,
@ -172,7 +175,7 @@ async function getArchive(
payload: Awaited<ReturnType<typeof getPayload>>,
tenantId: number // Now required
) {
const where: Record<string, unknown> = {
const where: Where = {
status: { equals: 'published' },
publishedAt: { exists: true },
tenant: { equals: tenantId },
@ -187,7 +190,7 @@ async function getArchive(
while (hasMore) {
const result = await payload.find({
collection: 'posts',
where,
where: where as Where,
sort: '-publishedAt',
page,
limit: pageSize,
@ -270,8 +273,8 @@ export async function GET(request: NextRequest) {
const includeArchive = searchParams.get('includeArchive') === 'true'
// Validate locale
const validLocales = ['de', 'en']
const locale = localeParam && validLocales.includes(localeParam) ? localeParam : 'de'
const validLocales: Locale[] = ['de', 'en']
const locale: Locale = localeParam && validLocales.includes(localeParam as Locale) ? (localeParam as Locale) : 'de'
// Validate and parse types
let types: NewsType | NewsType[] | undefined

View file

@ -50,7 +50,7 @@ export async function GET(
}
// Generate vCard 3.0
const vcard = generateVCard(member)
const vcard = generateVCard(member as TeamMember)
// Return as downloadable file
const filename = `${member.slug || member.name?.toLowerCase().replace(/\s+/g, '-')}.vcf`

View file

@ -1,7 +1,10 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config'
type Locale = 'de' | 'en' | 'all'
/**
* Team API
*
@ -44,10 +47,11 @@ export async function GET(request: NextRequest) {
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 100)
const page = parseInt(searchParams.get('page') || '1')
const sort = searchParams.get('sort') || 'order'
const locale = (searchParams.get('locale') as 'de' | 'en') || 'de'
const localeParam = searchParams.get('locale')
const locale: Locale = (localeParam === 'de' || localeParam === 'en') ? localeParam : 'de'
// Build where clause
const where: Record<string, unknown> = {
const where: Where = {
tenant: { equals: parseInt(tenantId) },
isActive: { equals: true },
}
@ -94,7 +98,7 @@ export async function GET(request: NextRequest) {
// Query team members
const result = await payload.find({
collection: 'team',
where,
where: where as Where,
sort: sortField,
limit,
page,

View file

@ -3,8 +3,11 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config'
import type { Media } from '@/payload-types'
type Locale = 'de' | 'en' | 'all'
import {
searchLimiter,
rateLimitHeaders,
@ -127,8 +130,8 @@ export async function GET(request: NextRequest) {
}
// Validate locale
const validLocales = ['de', 'en']
const locale = localeParam && validLocales.includes(localeParam) ? localeParam : 'de'
const validLocales: Locale[] = ['de', 'en']
const locale: Locale = localeParam && validLocales.includes(localeParam as Locale) ? (localeParam as Locale) : 'de'
// Validate type if provided
if (typeParam && !TIMELINE_TYPES.includes(typeParam as TimelineType)) {
@ -139,7 +142,7 @@ export async function GET(request: NextRequest) {
}
// Build where clause
const where: Record<string, unknown> = {
const where: Where = {
status: { equals: 'published' },
tenant: { equals: tenantId },
}
@ -160,7 +163,7 @@ export async function GET(request: NextRequest) {
// Execute query
const result = await payload.find({
collection: 'timelines',
where,
where: where as Where,
sort: '-updatedAt',
limit: slugParam ? 1 : 100, // Single or list
locale,

View file

@ -3,8 +3,11 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import type { Where } from 'payload'
import config from '@payload-config'
import type { Media } from '@/payload-types'
type Locale = 'de' | 'en' | 'all'
import {
searchLimiter,
rateLimitHeaders,
@ -122,8 +125,8 @@ export async function GET(request: NextRequest) {
}
// Validate locale
const validLocales = ['de', 'en']
const locale = localeParam && validLocales.includes(localeParam) ? localeParam : 'de'
const validLocales: Locale[] = ['de', 'en']
const locale: Locale = localeParam && validLocales.includes(localeParam as Locale) ? (localeParam as Locale) : 'de'
// Validate type if provided
if (typeParam && !WORKFLOW_TYPES.includes(typeParam as WorkflowType)) {
@ -142,7 +145,7 @@ export async function GET(request: NextRequest) {
}
// Build where clause
const where: Record<string, unknown> = {
const where: Where = {
status: { equals: 'published' },
tenant: { equals: tenantId },
}
@ -163,7 +166,7 @@ export async function GET(request: NextRequest) {
// Execute query
const result = await payload.find({
collection: 'workflows',
where,
where: where as Where,
sort: '-updatedAt',
limit: slugParam ? 1 : 100, // Single or list
locale,

View file

@ -12,8 +12,10 @@
*/
import { getPayload } from 'payload'
import type { Where } from 'payload'
import configPromise from '@payload-config'
import { NextRequest, NextResponse } from 'next/server'
import type { EmailLog } from '@/payload-types'
import { logAccessDenied } from '@/lib/audit/audit-service'
import { maskSmtpError } from '@/lib/security/data-masking'
@ -89,7 +91,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
const periodDate = getPeriodDate(period)
// Basis-Where für alle Queries
const baseWhere: Record<string, unknown> = {
const baseWhere: Where = {
createdAt: { greater_than_equal: periodDate.toISOString() },
}
@ -101,7 +103,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
// Gesamt
payload.count({
collection: 'email-logs',
where: baseWhere,
where: baseWhere as Where,
}),
// Gesendet
payload.count({
@ -161,7 +163,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
successRate,
},
bySource: sourceStats,
recentFailures: recentFailed.docs.map((doc: Record<string, unknown>) => ({
recentFailures: recentFailed.docs.map((doc: EmailLog) => ({
id: doc.id,
to: doc.to,
subject: doc.subject,

View file

@ -56,7 +56,7 @@ export class NewsletterService {
email: string
firstName?: string
lastName?: string
interests?: string[]
interests?: ('general' | 'blog' | 'products' | 'offers' | 'events')[]
source?: string
ipAddress?: string
userAgent?: string
@ -245,12 +245,14 @@ export class NewsletterService {
})
// Tenant-ID ermitteln
const tenantId = typeof subscriber.tenant === 'object'
const tenantId = typeof subscriber.tenant === 'object' && subscriber.tenant
? subscriber.tenant.id
: subscriber.tenant
// Willkommens-E-Mail senden
if (tenantId) {
await this.sendWelcomeEmail(tenantId as number, subscriber)
}
return {
success: true,
@ -308,7 +310,7 @@ export class NewsletterService {
}
// Tenant-ID ermitteln
const tenantId = typeof subscriber.tenant === 'object'
const tenantId = typeof subscriber.tenant === 'object' && subscriber.tenant
? subscriber.tenant.id
: subscriber.tenant

View file

@ -308,12 +308,12 @@ export default buildConfig({
// Fix für TypeScript Types Generation - das Plugin braucht explizite relationTo Angaben
redirectRelationships: ['pages'],
formSubmissionOverrides: {
...formSubmissionOverrides,
...(formSubmissionOverrides as Record<string, unknown>),
hooks: {
beforeChange: [formSubmissionBeforeChange],
afterChange: [sendFormNotification],
},
},
} as Parameters<typeof formBuilderPlugin>[0]['formSubmissionOverrides'],
}),
redirectsPlugin({
collections: ['pages'],
@ -330,10 +330,6 @@ export default buildConfig({
title: 'Payload CMS API',
version: '1.0.0',
description: 'Multi-Tenant CMS API für porwoll.de, complexcaresolutions.de, gunshin.de und zweitmein.ng',
contact: {
name: 'C2S GmbH',
url: 'https://complexcaresolutions.de',
},
},
}),
// Swagger UI unter /api/docs