Files
accounted/lib/extensions/types.ts
T
Jakob Wennberg ef5a84a5d5 feat: make extension system packageable via enriched ExtensionContext
Enrich ExtensionContext with supabase, emit(), settings, storage, log,
and services so extensions can receive everything through dependency
injection instead of importing core modules directly.

- Add context factory and inject context into event handlers via registry
- Move supplier invoice journal entry creation to core event handler
- Add services.ingestTransactions to ExtensionContext for enable-banking
- Create catch-all API route for extension-declared apiRoutes
- Migrate 5 extensions to accept context with dynamic import fallbacks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:24:21 +01:00

182 lines
5.1 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
import type { CoreEvent, CoreEventType } from '@/lib/events/types'
import type { EntityType, RawTransaction, IngestResult } from '@/types'
// ============================================================
// Extension Marketplace Types
// ============================================================
/** Extension category for marketplace grouping */
export type ExtensionCategory = 'accounting' | 'reports' | 'import' | 'operations'
/** Sector slugs for extension organization */
export type SectorSlug = 'general' | 'restaurant' | 'construction' | 'hotel' | 'tech' | 'ecommerce'
/** How an extension gets its data */
export type ExtensionDataPattern = 'core' | 'manual' | 'both'
/** Extension metadata for the marketplace and workspace routing */
export interface ExtensionDefinition {
slug: string
name: string
sector: SectorSlug
category: ExtensionCategory
description: string
longDescription: string
icon: string
entityTypes?: EntityType[]
dataPattern: ExtensionDataPattern
readsCoreTables?: string[]
hasOwnData?: boolean
}
/** Sector definition with its extensions */
export interface Sector {
slug: SectorSlug
name: string
icon: string
description: string
extensions: ExtensionDefinition[]
}
/** Database row for extension toggle state */
export interface ExtensionToggle {
id: string
user_id: string
sector_slug: string
extension_slug: string
enabled: boolean
created_at: string
updated_at: string
}
// ============================================================
// Extension Interface & Supporting Types
// ============================================================
/** A route exposed by an extension (page route) */
export interface RouteDefinition {
path: string
label: string
}
/** An API route exposed by an extension */
export interface ApiRouteDefinition {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
path: string
handler: (request: Request, ctx?: ExtensionContext) => Promise<Response>
}
/** Sidebar navigation item added by an extension */
export interface SidebarItem {
label: string
icon?: string
path: string
order?: number
}
/** Report type added by an extension */
export interface ReportDefinition {
id: string
name: string
description: string
}
/** Settings panel exposed by an extension */
export interface SettingsPanelDefinition {
label: string
path: string
}
/** Tax code definition added by an extension */
export interface TaxCodeDefinition {
code: string
rate: number
description: string
}
/** Dimension type definition added by an extension */
export interface DimensionDefinition {
id: string
name: string
description: string
}
/** Mapping rule type added by an extension */
export interface MappingRuleTypeDefinition {
id: string
name: string
description: string
}
/** Event handler registration for an extension */
export interface ExtensionEventHandler {
eventType: CoreEventType
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: (payload: any, ctx?: ExtensionContext) => Promise<void> | void
}
/** Logger interface for extensions */
export interface ExtensionLogger {
info(message: string, ...args: unknown[]): void
warn(message: string, ...args: unknown[]): void
error(message: string, ...args: unknown[]): void
}
/** Settings accessor for extension-scoped key-value data */
export interface ExtensionSettings {
get<T>(key?: string): Promise<T | null>
set<T>(key: string, value: T): Promise<void>
}
/** Storage accessor wrapping Supabase storage */
export interface ExtensionStorage {
download(bucket: string, path: string): Promise<{ data: Blob | null; error?: string }>
upload(bucket: string, path: string, data: ArrayBuffer, options?: { contentType?: string }): Promise<{ path: string; error?: string }>
getPublicUrl(bucket: string, path: string): string
}
/** Core services exposed to extensions */
export interface ExtensionServices {
ingestTransactions(supabase: SupabaseClient, userId: string, raw: RawTransaction[]): Promise<IngestResult>
}
/** Context passed to extension lifecycle hooks and event handlers */
export interface ExtensionContext {
userId: string
extensionId: string
supabase: SupabaseClient
emit(event: CoreEvent): Promise<void>
settings: ExtensionSettings
storage: ExtensionStorage
log: ExtensionLogger
services: ExtensionServices
}
/**
* Extension interface — the contract for all add-ons.
*
* Extensions declare what they provide (routes, event handlers, sidebar items, etc.)
* and the registry wires them into the system.
*/
export interface Extension {
id: string
name: string
version: string
// Surfaces
routes?: RouteDefinition[]
apiRoutes?: ApiRouteDefinition[]
sidebarItems?: SidebarItem[]
eventHandlers?: ExtensionEventHandler[]
mappingRuleTypes?: MappingRuleTypeDefinition[]
reportTypes?: ReportDefinition[]
settingsPanel?: SettingsPanelDefinition
taxCodes?: TaxCodeDefinition[]
dimensionTypes?: DimensionDefinition[]
// Lifecycle hooks
onInstall?(ctx: ExtensionContext): Promise<void>
onUninstall?(ctx: ExtensionContext): Promise<void>
}