Files
accounted/lib/extensions/context-factory.ts
T
Jakob WennbergandClaude Opus 4.6 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

123 lines
3.3 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
import type { CoreEvent } from '@/lib/events/types'
import { eventBus } from '@/lib/events/bus'
import { ingestTransactions } from '@/lib/transactions/ingest'
import type {
ExtensionContext,
ExtensionLogger,
ExtensionSettings,
ExtensionStorage,
ExtensionServices,
} from './types'
/**
* Create a prefixed logger for an extension.
*/
function createLogger(extensionId: string): ExtensionLogger {
const prefix = `[${extensionId}]`
return {
info: (message: string, ...args: unknown[]) => console.log(prefix, message, ...args),
warn: (message: string, ...args: unknown[]) => console.warn(prefix, message, ...args),
error: (message: string, ...args: unknown[]) => console.error(prefix, message, ...args),
}
}
/**
* Create a settings accessor scoped to a specific extension.
*/
function createSettings(
supabase: SupabaseClient,
userId: string,
extensionId: string
): ExtensionSettings {
return {
async get<T>(key?: string): Promise<T | null> {
const lookupKey = key ?? 'settings'
const { data } = await supabase
.from('extension_data')
.select('value')
.eq('user_id', userId)
.eq('extension_id', extensionId)
.eq('key', lookupKey)
.single()
return (data?.value as T) ?? null
},
async set<T>(key: string, value: T): Promise<void> {
await supabase
.from('extension_data')
.upsert(
{
user_id: userId,
extension_id: extensionId,
key,
value,
},
{ onConflict: 'user_id,extension_id,key' }
)
},
}
}
/**
* Create a storage accessor wrapping Supabase storage.
*/
function createStorage(supabase: SupabaseClient): ExtensionStorage {
return {
async download(bucket: string, path: string) {
const { data, error } = await supabase.storage
.from(bucket)
.download(path)
return { data, error: error?.message }
},
async upload(bucket: string, path: string, data: ArrayBuffer, options?: { contentType?: string }) {
const { error } = await supabase.storage
.from(bucket)
.upload(path, data, options ? { contentType: options.contentType } : undefined)
if (error) return { path: '', error: error.message }
return { path }
},
getPublicUrl(bucket: string, path: string): string {
const { data } = supabase.storage
.from(bucket)
.getPublicUrl(path)
return data.publicUrl
},
}
}
/**
* Create core services exposed to extensions.
*/
function createServices(): ExtensionServices {
return {
ingestTransactions,
}
}
/**
* Build a fully populated ExtensionContext.
*
* The context gives extensions access to Supabase, event emission, settings,
* storage, logging, and core services — without importing from core modules.
*/
export function createExtensionContext(
supabase: SupabaseClient,
userId: string,
extensionId: string
): ExtensionContext {
return {
userId,
extensionId,
supabase,
emit: (event: CoreEvent) => eventBus.emit(event),
settings: createSettings(supabase, userId, extensionId),
storage: createStorage(supabase),
log: createLogger(extensionId),
services: createServices(),
}
}