import type { SupabaseClient } from '@supabase/supabase-js'; import type { Note, CreateNoteInput, UpdateNoteInput, Context } from './types'; export type ApiConfig = { apiBase: string; supabaseClient?: SupabaseClient; supabaseAnonKey?: string; }; let config: ApiConfig = { apiBase: '/api', }; /** * Configure the notes API client * * @example * ```ts * import { createClient } from '@supabase/supabase-js'; * import { configureNotesApi } from '@micahgoodman/notes'; * * const supabase = createClient(url, key); * configureNotesApi({ * apiBase: process.env.VITE_API_BASE, * supabaseClient: supabase, * supabaseAnonKey: process.env.VITE_SUPABASE_ANON_KEY * }); * ``` */ export function configureNotesApi(newConfig: Partial) { config = { ...config, ...newConfig }; } function url(path: string): string { const base = config.apiBase; if (base.endsWith('/') && path.startsWith('/')) return base + path.slice(1); if (!base.endsWith('/') && !path.startsWith('/')) return `${base}/${path}`; return base + path; } function isSupabaseUrl(base: string): boolean { return /\.supabase\.co\//.test(base) || /127\.0\.0\.1|localhost/.test(base); } async function buildHeaders(extra?: HeadersInit): Promise { const headers: Record = {}; // Include Supabase auth headers when targeting Edge Functions if (isSupabaseUrl(config.apiBase) && config.supabaseClient && config.supabaseAnonKey) { const { data: { session } } = await config.supabaseClient.auth.getSession(); const token = session?.access_token || config.supabaseAnonKey; headers['apikey'] = config.supabaseAnonKey; headers['Authorization'] = `Bearer ${token}`; } if (extra) { const e = extra as Record; for (const k in e) headers[k] = e[k]; } return headers; } async function json(res: Response): Promise { const raw = await res.text(); let data: any = undefined; try { data = raw ? JSON.parse(raw) : undefined; } catch (_) { if (!res.ok) { throw new Error(`HTTP ${res.status} non-JSON response: ${raw.slice(0, 200)}`); } return (undefined as unknown) as T; } if (!res.ok) { const msg = (data && (data.error || data.message)) || raw; throw new Error(`HTTP ${res.status}: ${typeof msg === 'string' ? msg.slice(0, 200) : JSON.stringify(msg).slice(0, 200)}`); } return (data as any)?.payload ?? (data as any); } /** * Fetch all notes */ export async function fetchNotes(): Promise { return fetch(url('/concepts?filter=note'), { headers: await buildHeaders() }).then(res => json(res)); } /** * Fetch notes by context (e.g., notes associated with a specific entity) */ export async function fetchNotesByContext(contextType: string, contextId: string): Promise { return fetch( url(`/concepts?filter=note&contextType=${encodeURIComponent(contextType)}&contextId=${encodeURIComponent(contextId)}`), { headers: await buildHeaders() } ).then(res => json(res)); } /** * Create a new note */ export async function createNote(input: CreateNoteInput): Promise<{ id: string }> { return fetch(url('/concepts?filter=note'), { method: 'POST', headers: await buildHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify(input) }).then(res => json<{ id: string }>(res)); } /** * Update an existing note */ export async function updateNote(id: string, input: UpdateNoteInput): Promise<{ ok: boolean }> { return fetch(url(`/concepts/${encodeURIComponent(id)}`), { method: 'PATCH', headers: await buildHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify(input) }).then(res => json<{ ok: boolean }>(res)); } /** * Delete a note */ export async function deleteNote(id: string): Promise<{ ok: boolean }> { return fetch(url(`/concepts/${encodeURIComponent(id)}`), { method: 'DELETE', headers: await buildHeaders() }).then(res => json<{ ok: boolean }>(res)); } /** * Fetch by identity key (for associations) */ export async function fetchByIdentity(identityKey: string): Promise { const params = new URLSearchParams({ filter: 'note', identityKey }); return fetch(url(`/concepts?${params.toString()}`), { headers: await buildHeaders() }).then(res => json(res)); } /** * Associate a note with another entity */ export async function associateNote(body: { parentType: string; parentId: string; childId: string }): Promise<{ ok: boolean }> { return fetch(url('/concepts?action=associate'), { method: 'POST', headers: await buildHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ ...body, childType: 'note' }), }).then(res => json<{ ok: boolean }>(res)); } /** * Disassociate a note from another entity */ export async function disassociateNote(body: { parentType: string; parentId: string; childId: string }): Promise<{ ok: boolean }> { return fetch(url('/concepts?action=disassociate'), { method: 'POST', headers: await buildHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ ...body, childType: 'note' }), }).then(res => json<{ ok: boolean }>(res)); } /** * Fetch parents that reference this note */ export async function fetchParentsOfNote(noteId: string): Promise> { const params = new URLSearchParams({ childType: 'note', childId: noteId, parentsOnly: 'true' }); return fetch(url(`/concepts?${params.toString()}`), { headers: await buildHeaders() }) .then(res => json>(res)); }