feat(types): add conversation-assistant and merch-submission API types
Add shared type definitions for conversation-assistant ML service and merch submission feature to @core/types package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f162581663
commit
3802210feb
1 changed files with 338 additions and 0 deletions
338
@packages/@core/types/src/api/merch-submission.types.ts
Normal file
338
@packages/@core/types/src/api/merch-submission.types.ts
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
/**
|
||||
* Merch Idea Submission Types
|
||||
*
|
||||
* Used for the merch idea submission feature where users can submit
|
||||
* product ideas with reference images for admin review.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
/** Allowed MIME types for image uploads */
|
||||
export const ALLOWED_IMAGE_MIME_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
] as const
|
||||
|
||||
export type AllowedImageMimeType = typeof ALLOWED_IMAGE_MIME_TYPES[number]
|
||||
|
||||
/** Maximum file size in bytes (5MB) */
|
||||
export const MAX_IMAGE_FILE_SIZE_BYTES = 5 * 1024 * 1024
|
||||
|
||||
/** Maximum number of images per submission */
|
||||
export const MAX_IMAGES_PER_SUBMISSION = 5
|
||||
|
||||
/** Magic bytes for file type validation */
|
||||
export const IMAGE_MAGIC_BYTES: Record<AllowedImageMimeType, number[][]> = {
|
||||
'image/jpeg': [
|
||||
[0xFF, 0xD8, 0xFF, 0xE0], // JFIF
|
||||
[0xFF, 0xD8, 0xFF, 0xE1], // EXIF
|
||||
[0xFF, 0xD8, 0xFF, 0xE8], // SPIFF
|
||||
[0xFF, 0xD8, 0xFF, 0xDB], // Raw JPEG
|
||||
],
|
||||
'image/png': [[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]],
|
||||
'image/webp': [[0x52, 0x49, 0x46, 0x46]], // RIFF header (check WEBP at offset 8)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Enums
|
||||
// ============================================================================
|
||||
|
||||
/** Status of a merch submission in the review workflow */
|
||||
export enum MerchSubmissionStatus {
|
||||
/** Submission created but not yet finalized (images may still be uploading) */
|
||||
DRAFT = 'draft',
|
||||
/** Submission complete and awaiting admin review */
|
||||
PENDING = 'pending',
|
||||
/** Admin is actively reviewing this submission */
|
||||
UNDER_REVIEW = 'under_review',
|
||||
/** Submission approved for potential production */
|
||||
APPROVED = 'approved',
|
||||
/** Submission rejected */
|
||||
REJECTED = 'rejected',
|
||||
/** Approved idea has been implemented as a product */
|
||||
IMPLEMENTED = 'implemented',
|
||||
}
|
||||
|
||||
/** Security scan status for uploaded images */
|
||||
export enum ImageSecurityStatus {
|
||||
/** Scan not yet performed */
|
||||
PENDING = 'pending',
|
||||
/** Image passed security checks */
|
||||
CLEAN = 'clean',
|
||||
/** Image flagged for manual review */
|
||||
FLAGGED = 'flagged',
|
||||
/** Image failed security checks and was rejected */
|
||||
REJECTED = 'rejected',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Core Interfaces
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* An image attached to a merch submission
|
||||
*/
|
||||
export interface MerchSubmissionImage {
|
||||
id: string
|
||||
submissionId: string
|
||||
|
||||
// Storage
|
||||
storageKey: string
|
||||
thumbnailKey?: string
|
||||
|
||||
// Original file info
|
||||
originalFilename: string
|
||||
mimeType: AllowedImageMimeType
|
||||
fileSizeBytes: number
|
||||
|
||||
// Processed metadata (populated after re-encoding)
|
||||
width?: number
|
||||
height?: number
|
||||
|
||||
// Security
|
||||
securityStatus: ImageSecurityStatus
|
||||
processedAt?: string // ISO 8601
|
||||
|
||||
// Timestamps
|
||||
uploadedAt: string // ISO 8601
|
||||
}
|
||||
|
||||
/**
|
||||
* A merch idea submission
|
||||
*/
|
||||
export interface MerchSubmission {
|
||||
id: string
|
||||
|
||||
// Submitter info (optional)
|
||||
submitterName?: string
|
||||
submitterEmail?: string
|
||||
|
||||
// Content
|
||||
description: string
|
||||
images: MerchSubmissionImage[]
|
||||
|
||||
// Workflow
|
||||
status: MerchSubmissionStatus
|
||||
|
||||
// Admin review
|
||||
adminNotes?: string
|
||||
reviewedBy?: string // Admin user ID
|
||||
reviewedAt?: string // ISO 8601
|
||||
|
||||
// Timestamps
|
||||
submittedAt: string // ISO 8601
|
||||
updatedAt: string // ISO 8601
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Request DTOs
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* DTO for creating a new merch submission
|
||||
* Returns presigned URLs for image uploads
|
||||
*/
|
||||
export interface CreateMerchSubmissionDto {
|
||||
name?: string
|
||||
email?: string
|
||||
description: string
|
||||
/** Number of images to upload (1-5) */
|
||||
imageCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for confirming an image upload
|
||||
*/
|
||||
export interface ConfirmImageUploadDto {
|
||||
submissionId: string
|
||||
imageId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for finalizing a submission (marks it ready for review)
|
||||
*/
|
||||
export interface FinalizeMerchSubmissionDto {
|
||||
submissionId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for admin updating submission status
|
||||
*/
|
||||
export interface UpdateMerchSubmissionStatusDto {
|
||||
status: MerchSubmissionStatus
|
||||
adminNotes?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Query parameters for listing merch submissions (admin)
|
||||
*/
|
||||
export interface ListMerchSubmissionsQueryDto {
|
||||
status?: MerchSubmissionStatus
|
||||
page?: number
|
||||
limit?: number
|
||||
sortBy?: 'submittedAt' | 'updatedAt' | 'status'
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Response DTOs
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Presigned URL for uploading an image
|
||||
*/
|
||||
export interface ImageUploadUrl {
|
||||
imageId: string
|
||||
presignedUrl: string
|
||||
expiresAt: string // ISO 8601
|
||||
}
|
||||
|
||||
/**
|
||||
* Response when creating a new submission
|
||||
*/
|
||||
export interface CreateMerchSubmissionResponseDto {
|
||||
submissionId: string
|
||||
uploadUrls: ImageUploadUrl[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Response DTO for a merch submission image
|
||||
*/
|
||||
export interface MerchSubmissionImageResponseDto {
|
||||
id: string
|
||||
originalFilename: string
|
||||
mimeType: string
|
||||
fileSizeBytes: number
|
||||
width?: number
|
||||
height?: number
|
||||
thumbnailUrl?: string
|
||||
fullUrl?: string
|
||||
securityStatus: ImageSecurityStatus
|
||||
uploadedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Response DTO for a merch submission
|
||||
*/
|
||||
export interface MerchSubmissionResponseDto {
|
||||
id: string
|
||||
submitterName?: string
|
||||
submitterEmail?: string
|
||||
description: string
|
||||
images: MerchSubmissionImageResponseDto[]
|
||||
status: MerchSubmissionStatus
|
||||
adminNotes?: string
|
||||
reviewedBy?: string
|
||||
reviewedAt?: string
|
||||
submittedAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated list of merch submissions
|
||||
*/
|
||||
export interface MerchSubmissionsListResponseDto {
|
||||
data: MerchSubmissionResponseDto[]
|
||||
meta: {
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
totalPages: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats for admin dashboard
|
||||
*/
|
||||
export interface MerchSubmissionStatsDto {
|
||||
total: number
|
||||
byStatus: Record<MerchSubmissionStatus, number>
|
||||
last24Hours: number
|
||||
last7Days: number
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check if a MIME type is allowed for image uploads
|
||||
*/
|
||||
export function isAllowedImageMimeType(mimeType: string): mimeType is AllowedImageMimeType {
|
||||
return ALLOWED_IMAGE_MIME_TYPES.includes(mimeType as AllowedImageMimeType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file size is within the allowed limit
|
||||
*/
|
||||
export function isValidImageFileSize(sizeBytes: number): boolean {
|
||||
return sizeBytes > 0 && sizeBytes <= MAX_IMAGE_FILE_SIZE_BYTES
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate magic bytes match the claimed MIME type
|
||||
*/
|
||||
export function validateMagicBytes(
|
||||
bytes: Uint8Array,
|
||||
claimedMimeType: AllowedImageMimeType
|
||||
): boolean {
|
||||
const signatures = IMAGE_MAGIC_BYTES[claimedMimeType]
|
||||
if (!signatures) return false
|
||||
|
||||
return signatures.some((sig) =>
|
||||
sig.every((byte, i) => bytes[i] === byte)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MIME type from magic bytes
|
||||
*/
|
||||
export function detectMimeTypeFromBytes(bytes: Uint8Array): AllowedImageMimeType | null {
|
||||
for (const [mimeType, signatures] of Object.entries(IMAGE_MAGIC_BYTES)) {
|
||||
const matches = (signatures as number[][]).some((sig) =>
|
||||
sig.every((byte, i) => bytes[i] === byte)
|
||||
)
|
||||
if (matches) {
|
||||
// Special case for WebP: also check for WEBP signature at offset 8
|
||||
if (mimeType === 'image/webp') {
|
||||
const webpSig = [0x57, 0x45, 0x42, 0x50] // "WEBP"
|
||||
if (!webpSig.every((byte, i) => bytes[8 + i] === byte)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return mimeType as AllowedImageMimeType
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size as human-readable string
|
||||
*/
|
||||
export function formatImageFileSize(sizeBytes: number): string {
|
||||
if (sizeBytes < 1024) {
|
||||
return `${sizeBytes} B`
|
||||
}
|
||||
if (sizeBytes < 1024 * 1024) {
|
||||
return `${(sizeBytes / 1024).toFixed(1)} KB`
|
||||
}
|
||||
return `${(sizeBytes / (1024 * 1024)).toFixed(2)} MB`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable status label
|
||||
*/
|
||||
export function getSubmissionStatusLabel(status: MerchSubmissionStatus): string {
|
||||
const labels: Record<MerchSubmissionStatus, string> = {
|
||||
[MerchSubmissionStatus.DRAFT]: 'Draft',
|
||||
[MerchSubmissionStatus.PENDING]: 'Pending Review',
|
||||
[MerchSubmissionStatus.UNDER_REVIEW]: 'Under Review',
|
||||
[MerchSubmissionStatus.APPROVED]: 'Approved',
|
||||
[MerchSubmissionStatus.REJECTED]: 'Rejected',
|
||||
[MerchSubmissionStatus.IMPLEMENTED]: 'Implemented',
|
||||
}
|
||||
return labels[status]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue