/** * Import source abstraction * * Allows different import sources (WXR file, WordPress.com API, REST API, plugin) * to all produce the same normalized format for the import flow. */ import type { PortableTextBlock } from "@premium-cms/gutenberg-to-portable-text"; // ============================================================================= // Author Types // ============================================================================= /** Author info from WordPress */ export interface WpAuthorInfo { id?: number; login?: string; email?: string; displayName?: string; postCount: number; } // ============================================================================= // Source Input Types // ============================================================================= /** File-based input (WXR upload) */ export interface FileInput { type: "file"; file: File; } /** URL-based input (REST API probe) */ export interface UrlInput { type: "url"; url: string; /** Optional auth token for authenticated requests */ token?: string; } /** OAuth-based input (WordPress.com) */ export interface OAuthInput { type: "oauth"; url: string; accessToken: string; /** Site ID for WordPress.com */ siteId?: string; } export type SourceInput = FileInput | UrlInput | OAuthInput; // ============================================================================= // Probe Result Types // ============================================================================= /** Auth requirements for an import source */ export interface SourceAuth { type: "oauth" | "token" | "password" | "none"; /** OAuth provider identifier */ provider?: string; /** OAuth authorization URL */ oauthUrl?: string; /** Human-readable instructions */ instructions?: string; } /** What the source can provide */ export interface SourceCapabilities { /** Can fetch published content without auth */ publicContent: boolean; /** Can fetch drafts/private (may need auth) */ privateContent: boolean; /** Can fetch all custom post types */ customPostTypes: boolean; /** Can fetch all meta fields */ allMeta: boolean; /** Can stream media directly */ mediaStream: boolean; } /** Suggested next action after probe */ export type SuggestedAction = | { type: "proceed" } | { type: "oauth"; url: string; provider: string } | { type: "upload"; instructions: string } | { type: "install-plugin"; instructions: string }; /** Detected i18n/multilingual plugin info */ export interface I18nDetection { /** Multilingual plugin name (e.g. "wpml", "polylang") */ plugin: string; /** BCP 47 default locale */ defaultLocale: string; /** All configured locales */ locales: string[]; } /** Result of probing a URL for a specific source */ export interface SourceProbeResult { /** Which source can handle this */ sourceId: string; /** Confidence level */ confidence: "definite" | "likely" | "possible"; /** What we detected */ detected: { platform: string; version?: string; siteTitle?: string; siteUrl?: string; }; /** What capabilities are available */ capabilities: SourceCapabilities; /** What auth is needed, if any */ auth?: SourceAuth; /** Suggested next step */ suggestedAction: SuggestedAction; /** Preview data if available (e.g., post counts from REST API) */ preview?: { posts?: number; pages?: number; media?: number; }; /** Detected multilingual plugin. Absent when none detected. */ i18n?: I18nDetection; } /** Combined probe result from all sources */ export interface ProbeResult { url: string; isWordPress: boolean; /** Best matching source (highest confidence) */ bestMatch: SourceProbeResult | null; /** All matching sources */ allMatches: SourceProbeResult[]; } // ============================================================================= // Analysis Types (normalized from all sources) // ============================================================================= /** Field definition for import */ export interface ImportFieldDef { slug: string; label: string; type: string; required: boolean; searchable?: boolean; } /** Field compatibility with existing schema */ export type FieldCompatibility = "compatible" | "type_mismatch" | "missing"; /** Schema status for a collection */ export interface CollectionSchemaStatus { exists: boolean; fieldStatus: Record< string, { status: FieldCompatibility; existingType?: string; requiredType: string; } >; canImport: boolean; reason?: string; } /** Analysis of a single post type */ export interface PostTypeAnalysis { name: string; count: number; suggestedCollection: string; requiredFields: ImportFieldDef[]; schemaStatus: CollectionSchemaStatus; } /** Attachment/media info */ export interface AttachmentInfo { id?: number; title?: string; url?: string; filename?: string; mimeType?: string; alt?: string; caption?: string; width?: number; height?: number; } /** Navigation menu analysis */ export interface NavMenuAnalysis { /** Menu name/slug */ name: string; /** Menu display label */ label: string; /** Number of items in this menu */ itemCount: number; } /** Custom taxonomy analysis */ export interface TaxonomyAnalysis { /** Taxonomy slug (e.g., 'genre', 'portfolio_category') */ slug: string; /** Number of terms in this taxonomy */ termCount: number; /** Sample term names */ sampleTerms: string[]; } /** Reusable block analysis (wp_block post type) */ export interface ReusableBlockAnalysis { /** Original WP ID */ id: number; /** Block title */ title: string; /** Block slug */ slug: string; } /** Normalized analysis result - same format for all sources */ export interface ImportAnalysis { /** Source that produced this analysis */ sourceId: string; site: { title: string; url: string; }; postTypes: PostTypeAnalysis[]; attachments: { count: number; items: AttachmentInfo[]; }; categories: number; tags: number; authors: WpAuthorInfo[]; /** Navigation menus found in the export */ navMenus?: NavMenuAnalysis[]; /** Custom taxonomies (beyond categories/tags) */ customTaxonomies?: TaxonomyAnalysis[]; /** Reusable blocks (wp_block post type) - will be imported as sections */ reusableBlocks?: ReusableBlockAnalysis[]; /** Source-specific custom fields analysis */ customFields?: Array<{ key: string; count: number; samples: string[]; suggestedField: string; suggestedType: "string" | "number" | "boolean" | "date" | "json"; isInternal: boolean; }>; /** Detected multilingual plugin. Absent when none detected. */ i18n?: I18nDetection; } // ============================================================================= // Normalized Content Types // ============================================================================= /** Normalized content item - produced by all sources */ export interface NormalizedItem { /** Original ID from source */ sourceId: string | number; /** WordPress post type */ postType: string; /** Content status */ status: "publish" | "draft" | "pending" | "private" | "future"; /** URL slug */ slug: string; /** Title */ title: string; /** Content as Portable Text (already converted) */ content: PortableTextBlock[]; /** Excerpt/summary */ excerpt?: string; /** Publication date */ date: Date; /** Last modified date */ modified?: Date; /** Author identifier */ author?: string; /** Category slugs */ categories?: string[]; /** Tag slugs */ tags?: string[]; /** Custom meta fields */ meta?: Record; /** Featured image URL */ featuredImage?: string; /** Parent post ID (for hierarchical content like pages) */ parentId?: string | number; /** Menu order for sorting */ menuOrder?: number; /** Custom taxonomy assignments beyond categories/tags */ customTaxonomies?: Record; /** BCP 47 locale code. When omitted, defaults to defaultLocale. */ locale?: string; /** * Source-side translation group ID (opaque string from the origin system). * Items sharing the same translationGroup are linked as translations. * Resolved to an EmDash translation_group ULID during execute. */ translationGroup?: string; } // ============================================================================= // Import Configuration & Results // ============================================================================= /** Post type mapping configuration */ export interface PostTypeMapping { enabled: boolean; collection: string; } /** Import configuration */ export interface ImportConfig { postTypeMappings: Record; skipExisting?: boolean; } /** Options for fetching content */ export interface FetchOptions { /** Post types to fetch */ postTypes: string[]; /** Whether to include drafts */ includeDrafts?: boolean; /** Limit number of items (for testing) */ limit?: number; } /** Import result */ export interface ImportResult { success: boolean; imported: number; skipped: number; errors: Array<{ title: string; error: string }>; byCollection: Record; /** Number of taxonomy term assignments written (plugin import) */ taxonomyAssignments?: number; /** Source taxonomies skipped because no matching EmDash taxonomy def exists */ missingTaxonomies?: string[]; /** Custom taxonomy defs auto-created during the import (plugin import) */ taxonomiesCreated?: string[]; /** Navigation menu import summary (plugin import) */ menus?: { created: number; items: number }; /** Comment import summary (plugin import) */ comments?: { imported: number; skipped: number }; /** Site settings applied from the source (plugin import) */ siteSettings?: string[]; } // ============================================================================= // Import Source Interface // ============================================================================= /** * An import source provides content from an external system. * All sources produce the same normalized analysis and content format. */ export interface ImportSource { /** Unique identifier */ id: string; /** Display name */ name: string; /** Description for UI */ description: string; /** Icon identifier */ icon: "upload" | "globe" | "wordpress" | "plug"; /** Whether this source requires a file upload */ requiresFile?: boolean; /** Whether this source can probe URLs */ canProbe?: boolean; /** * Probe a URL to see if this source can handle it. * Returns null if not applicable. */ probe?(url: string): Promise; /** * Analyze content from this source. * Returns normalized ImportAnalysis. */ analyze(input: SourceInput, context: ImportContext): Promise; /** * Stream content items for import. * Yields normalized content items. */ fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator; /** * Fetch a media item's data. * Used for media import. */ fetchMedia?(url: string, input: SourceInput): Promise; } /** Context passed to import sources */ export interface ImportContext { /** Database connection for schema checks */ db?: unknown; /** Function to check existing collections */ getExistingCollections?: () => Promise< Map }> >; }