Mega push vol 4
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database, CalendarEvent } from '$lib/supabase/types';
|
||||
import { createLogger } from '$lib/utils/logger';
|
||||
|
||||
const log = createLogger('api.calendar');
|
||||
|
||||
export async function fetchEvents(
|
||||
supabase: SupabaseClient<Database>,
|
||||
@@ -15,7 +18,10 @@ export async function fetchEvents(
|
||||
.lte('end_time', endDate.toISOString())
|
||||
.order('start_time');
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('fetchEvents failed', { error, data: { orgId } });
|
||||
throw error;
|
||||
}
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
@@ -47,7 +53,10 @@ export async function createEvent(
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('createEvent failed', { error, data: { orgId, title: event.title } });
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -57,7 +66,10 @@ export async function updateEvent(
|
||||
updates: Partial<Pick<CalendarEvent, 'title' | 'description' | 'start_time' | 'end_time' | 'all_day' | 'color'>>
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('calendar_events').update(updates).eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateEvent failed', { error, data: { id, updates } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteEvent(
|
||||
@@ -65,7 +77,10 @@ export async function deleteEvent(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('calendar_events').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('deleteEvent failed', { error, data: { id } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeToEvents(
|
||||
@@ -85,8 +100,11 @@ export function getMonthDays(year: number, month: number): Date[] {
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const days: Date[] = [];
|
||||
|
||||
// Week starts on Monday (0=Mon, 6=Sun)
|
||||
let startDayOfWeek = firstDay.getDay() - 1;
|
||||
if (startDayOfWeek < 0) startDayOfWeek = 6; // Sunday becomes 6
|
||||
|
||||
// Add days from previous month to fill first week
|
||||
const startDayOfWeek = firstDay.getDay();
|
||||
for (let i = startDayOfWeek - 1; i >= 0; i--) {
|
||||
days.push(new Date(year, month, -i));
|
||||
}
|
||||
@@ -96,8 +114,8 @@ export function getMonthDays(year: number, month: number): Date[] {
|
||||
days.push(new Date(year, month, i));
|
||||
}
|
||||
|
||||
// Add days from next month to fill last week
|
||||
const remainingDays = 42 - days.length; // 6 weeks * 7 days
|
||||
// Add days from next month to fill last week (up to 6 rows)
|
||||
const remainingDays = 42 - days.length;
|
||||
for (let i = 1; i <= remainingDays; i++) {
|
||||
days.push(new Date(year, month + 1, i));
|
||||
}
|
||||
|
||||
152
src/lib/api/document-locks.ts
Normal file
152
src/lib/api/document-locks.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database } from '$lib/supabase/types';
|
||||
import { createLogger } from '$lib/utils/logger';
|
||||
|
||||
const log = createLogger('api.document-locks');
|
||||
|
||||
const LOCK_EXPIRY_SECONDS = 60;
|
||||
const HEARTBEAT_INTERVAL_MS = 30_000; // 30 seconds
|
||||
|
||||
export interface LockInfo {
|
||||
isLocked: boolean;
|
||||
lockedBy: string | null;
|
||||
lockedByName: string | null;
|
||||
isOwnLock: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current lock status for a document.
|
||||
* Only returns active locks (heartbeat within LOCK_EXPIRY_SECONDS).
|
||||
*/
|
||||
export async function getLockInfo(
|
||||
supabase: SupabaseClient<Database>,
|
||||
documentId: string,
|
||||
currentUserId: string
|
||||
): Promise<LockInfo> {
|
||||
const cutoff = new Date(Date.now() - LOCK_EXPIRY_SECONDS * 1000).toISOString();
|
||||
|
||||
const { data: lock } = await supabase
|
||||
.from('document_locks')
|
||||
.select(`
|
||||
id,
|
||||
document_id,
|
||||
user_id,
|
||||
locked_at,
|
||||
last_heartbeat,
|
||||
profiles:user_id (full_name, email)
|
||||
`)
|
||||
.eq('document_id', documentId)
|
||||
.gt('last_heartbeat', cutoff)
|
||||
.single();
|
||||
|
||||
if (!lock) {
|
||||
return { isLocked: false, lockedBy: null, lockedByName: null, isOwnLock: false };
|
||||
}
|
||||
|
||||
const profile = (lock as any).profiles; // join type not inferred by Supabase
|
||||
return {
|
||||
isLocked: true,
|
||||
lockedBy: lock.user_id,
|
||||
lockedByName: profile?.full_name || profile?.email || 'Someone',
|
||||
isOwnLock: lock.user_id === currentUserId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a lock on a document. Cleans up expired locks first.
|
||||
* Returns true if lock was acquired, false if someone else holds it.
|
||||
*/
|
||||
export async function acquireLock(
|
||||
supabase: SupabaseClient<Database>,
|
||||
documentId: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
const cutoff = new Date(Date.now() - LOCK_EXPIRY_SECONDS * 1000).toISOString();
|
||||
|
||||
// Delete expired locks for this document
|
||||
await supabase
|
||||
.from('document_locks')
|
||||
.delete()
|
||||
.eq('document_id', documentId)
|
||||
.lt('last_heartbeat', cutoff);
|
||||
|
||||
// Try to insert our lock
|
||||
const { error } = await supabase
|
||||
.from('document_locks')
|
||||
.insert({
|
||||
document_id: documentId,
|
||||
user_id: userId,
|
||||
locked_at: new Date().toISOString(),
|
||||
last_heartbeat: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
if (error.code === '23505') {
|
||||
// Unique constraint violation — someone else holds the lock
|
||||
log.debug('Lock already held', { data: { documentId } });
|
||||
return false;
|
||||
}
|
||||
log.error('acquireLock failed', { error, data: { documentId } });
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info('Lock acquired', { data: { documentId, userId } });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a heartbeat to keep the lock alive.
|
||||
*/
|
||||
export async function heartbeatLock(
|
||||
supabase: SupabaseClient<Database>,
|
||||
documentId: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
const { error } = await supabase
|
||||
.from('document_locks')
|
||||
.update({ last_heartbeat: new Date().toISOString() })
|
||||
.eq('document_id', documentId)
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (error) {
|
||||
log.error('heartbeatLock failed', { error, data: { documentId } });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a lock on a document.
|
||||
*/
|
||||
export async function releaseLock(
|
||||
supabase: SupabaseClient<Database>,
|
||||
documentId: string,
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('document_locks')
|
||||
.delete()
|
||||
.eq('document_id', documentId)
|
||||
.eq('user_id', userId);
|
||||
|
||||
if (error) {
|
||||
log.error('releaseLock failed', { error, data: { documentId } });
|
||||
} else {
|
||||
log.info('Lock released', { data: { documentId, userId } });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a heartbeat interval. Returns a cleanup function.
|
||||
*/
|
||||
export function startHeartbeat(
|
||||
supabase: SupabaseClient<Database>,
|
||||
documentId: string,
|
||||
userId: string
|
||||
): () => void {
|
||||
const interval = setInterval(() => {
|
||||
heartbeatLock(supabase, documentId, userId);
|
||||
}, HEARTBEAT_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database, Document } from '$lib/supabase/types';
|
||||
import { createLogger } from '$lib/utils/logger';
|
||||
|
||||
export interface DocumentWithChildren extends Document {
|
||||
children?: DocumentWithChildren[];
|
||||
}
|
||||
const log = createLogger('api.documents');
|
||||
|
||||
export async function fetchDocuments(
|
||||
supabase: SupabaseClient<Database>,
|
||||
@@ -16,7 +15,11 @@ export async function fetchDocuments(
|
||||
.order('type', { ascending: false }) // folders first
|
||||
.order('name');
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('fetchDocuments failed', { error, data: { orgId } });
|
||||
throw error;
|
||||
}
|
||||
log.debug('fetchDocuments ok', { data: { count: data?.length ?? 0 } });
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
@@ -41,7 +44,11 @@ export async function createDocument(
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('createDocument failed', { error, data: { orgId, name, type, parentId } });
|
||||
throw error;
|
||||
}
|
||||
log.info('createDocument ok', { data: { id: data.id, name, type } });
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -57,7 +64,10 @@ export async function updateDocument(
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateDocument failed', { error, data: { id, updates } });
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -66,7 +76,10 @@ export async function deleteDocument(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('documents').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('deleteDocument failed', { error, data: { id } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveDocument(
|
||||
@@ -79,30 +92,12 @@ export async function moveDocument(
|
||||
.update({ parent_id: newParentId, updated_at: new Date().toISOString() })
|
||||
.eq('id', id);
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('moveDocument failed', { error, data: { id, newParentId } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDocumentTree(documents: Document[]): DocumentWithChildren[] {
|
||||
const map = new Map<string, DocumentWithChildren>();
|
||||
const roots: DocumentWithChildren[] = [];
|
||||
|
||||
// First pass: create map
|
||||
documents.forEach((doc) => {
|
||||
map.set(doc.id, { ...doc, children: [] });
|
||||
});
|
||||
|
||||
// Second pass: build tree
|
||||
documents.forEach((doc) => {
|
||||
const node = map.get(doc.id)!;
|
||||
if (doc.parent_id && map.has(doc.parent_id)) {
|
||||
map.get(doc.parent_id)!.children!.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
export function subscribeToDocuments(
|
||||
supabase: SupabaseClient<Database>,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database, KanbanBoard, KanbanColumn, KanbanCard } from '$lib/supabase/types';
|
||||
import { createLogger } from '$lib/utils/logger';
|
||||
|
||||
const log = createLogger('api.kanban');
|
||||
|
||||
export interface ColumnWithCards extends KanbanColumn {
|
||||
cards: KanbanCard[];
|
||||
@@ -19,7 +22,11 @@ export async function fetchBoards(
|
||||
.eq('org_id', orgId)
|
||||
.order('created_at');
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('fetchBoards failed', { error, data: { orgId } });
|
||||
throw error;
|
||||
}
|
||||
log.debug('fetchBoards ok', { data: { count: data?.length ?? 0 } });
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
@@ -33,7 +40,10 @@ export async function fetchBoardWithColumns(
|
||||
.eq('id', boardId)
|
||||
.single();
|
||||
|
||||
if (boardError) throw boardError;
|
||||
if (boardError) {
|
||||
log.error('fetchBoardWithColumns failed (board)', { error: boardError, data: { boardId } });
|
||||
throw boardError;
|
||||
}
|
||||
if (!board) return null;
|
||||
|
||||
const { data: columns, error: colError } = await supabase
|
||||
@@ -42,22 +52,55 @@ export async function fetchBoardWithColumns(
|
||||
.eq('board_id', boardId)
|
||||
.order('position');
|
||||
|
||||
if (colError) throw colError;
|
||||
if (colError) {
|
||||
log.error('fetchBoardWithColumns failed (columns)', { error: colError, data: { boardId } });
|
||||
throw colError;
|
||||
}
|
||||
|
||||
const columnIds = (columns ?? []).map((c) => c.id);
|
||||
|
||||
const { data: cards, error: cardError } = await supabase
|
||||
.from('kanban_cards')
|
||||
.select('*')
|
||||
.in('column_id', (columns ?? []).map((c) => c.id))
|
||||
.in('column_id', columnIds)
|
||||
.order('position');
|
||||
|
||||
if (cardError) throw cardError;
|
||||
if (cardError) {
|
||||
log.error('fetchBoardWithColumns failed (cards)', { error: cardError, data: { boardId } });
|
||||
throw cardError;
|
||||
}
|
||||
|
||||
const cardsByColumn = new Map<string, KanbanCard[]>();
|
||||
// Fetch tags for all cards in one query
|
||||
const cardIds = (cards ?? []).map((c) => c.id);
|
||||
let cardTagsMap = new Map<string, { id: string; name: string; color: string | null }[]>();
|
||||
|
||||
if (cardIds.length > 0) {
|
||||
const { data: cardTags } = await supabase
|
||||
.from('card_tags')
|
||||
.select('card_id, tags:tag_id (id, name, color)')
|
||||
.in('card_id', cardIds);
|
||||
|
||||
(cardTags ?? []).forEach((ct: any) => {
|
||||
const tag = Array.isArray(ct.tags) ? ct.tags[0] : ct.tags;
|
||||
if (!tag) return;
|
||||
if (!cardTagsMap.has(ct.card_id)) {
|
||||
cardTagsMap.set(ct.card_id, []);
|
||||
}
|
||||
cardTagsMap.get(ct.card_id)!.push(tag);
|
||||
});
|
||||
}
|
||||
|
||||
const cardsByColumn = new Map<string, (KanbanCard & { tags?: { id: string; name: string; color: string | null }[] })[]>();
|
||||
(cards ?? []).forEach((card) => {
|
||||
if (!cardsByColumn.has(card.column_id)) {
|
||||
cardsByColumn.set(card.column_id, []);
|
||||
const colId = card.column_id;
|
||||
if (!colId) return;
|
||||
if (!cardsByColumn.has(colId)) {
|
||||
cardsByColumn.set(colId, []);
|
||||
}
|
||||
cardsByColumn.get(card.column_id)!.push(card);
|
||||
cardsByColumn.get(colId)!.push({
|
||||
...card,
|
||||
tags: cardTagsMap.get(card.id) ?? []
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -74,13 +117,17 @@ export async function createBoard(
|
||||
orgId: string,
|
||||
name: string
|
||||
): Promise<KanbanBoard> {
|
||||
log.info('createBoard', { data: { orgId, name } });
|
||||
const { data, error } = await supabase
|
||||
.from('kanban_boards')
|
||||
.insert({ org_id: orgId, name })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('createBoard failed', { error, data: { orgId, name } });
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Create default columns
|
||||
const defaultColumns = ['To Do', 'In Progress', 'Done'];
|
||||
@@ -101,7 +148,10 @@ export async function updateBoard(
|
||||
name: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_boards').update({ name }).eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateBoard failed', { error, data: { id, name } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteBoard(
|
||||
@@ -109,7 +159,10 @@ export async function deleteBoard(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_boards').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('deleteBoard failed', { error, data: { id } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createColumn(
|
||||
@@ -124,7 +177,10 @@ export async function createColumn(
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('createColumn failed', { error, data: { boardId, name, position } });
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -134,7 +190,10 @@ export async function updateColumn(
|
||||
updates: Partial<Pick<KanbanColumn, 'name' | 'position' | 'color'>>
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_columns').update(updates).eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateColumn failed', { error, data: { id, updates } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteColumn(
|
||||
@@ -142,7 +201,10 @@ export async function deleteColumn(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_columns').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('deleteColumn failed', { error, data: { id } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCard(
|
||||
@@ -163,7 +225,10 @@ export async function createCard(
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('createCard failed', { error, data: { columnId, title, position } });
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -173,7 +238,10 @@ export async function updateCard(
|
||||
updates: Partial<Pick<KanbanCard, 'title' | 'description' | 'column_id' | 'position' | 'due_date' | 'color'>>
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_cards').update(updates).eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateCard failed', { error, data: { id, updates } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCard(
|
||||
@@ -181,7 +249,10 @@ export async function deleteCard(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('kanban_cards').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('deleteCard failed', { error, data: { id } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveCard(
|
||||
@@ -190,12 +261,48 @@ export async function moveCard(
|
||||
newColumnId: string,
|
||||
newPosition: number
|
||||
): Promise<void> {
|
||||
const { error } = await supabase
|
||||
// Fetch all cards in the target column (ordered by position)
|
||||
const { data: targetCards, error: fetchErr } = await supabase
|
||||
.from('kanban_cards')
|
||||
.update({ column_id: newColumnId, position: newPosition })
|
||||
.eq('id', cardId);
|
||||
.select('id, position')
|
||||
.eq('column_id', newColumnId)
|
||||
.order('position');
|
||||
|
||||
if (error) throw error;
|
||||
if (fetchErr) {
|
||||
log.error('moveCard: failed to fetch target column cards', { error: fetchErr });
|
||||
throw fetchErr;
|
||||
}
|
||||
|
||||
// Remove the moved card from the list if it's already in this column
|
||||
const otherCards = (targetCards ?? []).filter((c) => c.id !== cardId);
|
||||
|
||||
// Insert at the new position and reassign sequential positions
|
||||
const reordered = [
|
||||
...otherCards.slice(0, newPosition),
|
||||
{ id: cardId },
|
||||
...otherCards.slice(newPosition),
|
||||
];
|
||||
|
||||
// Batch update: move card to column + set position, then update siblings
|
||||
const updates = reordered.map((c, i) => {
|
||||
if (c.id === cardId) {
|
||||
return supabase
|
||||
.from('kanban_cards')
|
||||
.update({ column_id: newColumnId, position: i })
|
||||
.eq('id', c.id);
|
||||
}
|
||||
return supabase
|
||||
.from('kanban_cards')
|
||||
.update({ position: i })
|
||||
.eq('id', c.id);
|
||||
});
|
||||
|
||||
const results = await Promise.all(updates);
|
||||
const failed = results.find((r) => r.error);
|
||||
if (failed?.error) {
|
||||
log.error('moveCard failed', { error: failed.error, data: { cardId, newColumnId, newPosition } });
|
||||
throw failed.error;
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeToBoard(
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { Database, Organization, MemberRole } from '$lib/supabase/types';
|
||||
import type { OrgWithRole } from '$lib/stores/organizations.svelte';
|
||||
import { createLogger } from '$lib/utils/logger';
|
||||
|
||||
export interface OrgWithRole extends Organization {
|
||||
role: MemberRole;
|
||||
memberCount?: number;
|
||||
}
|
||||
|
||||
const log = createLogger('api.organizations');
|
||||
|
||||
export async function fetchUserOrganizations(
|
||||
supabase: SupabaseClient<Database>
|
||||
@@ -20,7 +27,10 @@ export async function fetchUserOrganizations(
|
||||
`)
|
||||
.not('joined_at', 'is', null);
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('fetchUserOrganizations failed', { error });
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (data ?? [])
|
||||
.filter((item) => item.organizations)
|
||||
@@ -35,13 +45,17 @@ export async function createOrganization(
|
||||
name: string,
|
||||
slug: string
|
||||
): Promise<Organization> {
|
||||
log.info('createOrganization', { data: { name, slug } });
|
||||
const { data, error } = await supabase
|
||||
.from('organizations')
|
||||
.insert({ name, slug })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('createOrganization failed', { error, data: { name, slug } });
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -57,7 +71,10 @@ export async function updateOrganization(
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateOrganization failed', { error, data: { id, updates } });
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -66,7 +83,10 @@ export async function deleteOrganization(
|
||||
id: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('organizations').delete().eq('id', id);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('deleteOrganization failed', { error, data: { id } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchOrgMembers(
|
||||
@@ -90,7 +110,10 @@ export async function fetchOrgMembers(
|
||||
`)
|
||||
.eq('org_id', orgId);
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('fetchOrgMembers failed', { error, data: { orgId } });
|
||||
throw error;
|
||||
}
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
@@ -108,6 +131,7 @@ export async function inviteMember(
|
||||
.single();
|
||||
|
||||
if (profileError || !profile) {
|
||||
log.warn('inviteMember: user not found', { data: { email } });
|
||||
throw new Error('User not found. They need to sign up first.');
|
||||
}
|
||||
|
||||
@@ -120,6 +144,7 @@ export async function inviteMember(
|
||||
.single();
|
||||
|
||||
if (existing) {
|
||||
log.warn('inviteMember: already a member', { data: { email, orgId } });
|
||||
throw new Error('User is already a member of this organization.');
|
||||
}
|
||||
|
||||
@@ -131,7 +156,10 @@ export async function inviteMember(
|
||||
joined_at: new Date().toISOString() // Auto-join for now
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('inviteMember failed', { error, data: { orgId, email, role } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMemberRole(
|
||||
@@ -144,7 +172,10 @@ export async function updateMemberRole(
|
||||
.update({ role })
|
||||
.eq('id', memberId);
|
||||
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('updateMemberRole failed', { error, data: { memberId, role } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeMember(
|
||||
@@ -152,7 +183,10 @@ export async function removeMember(
|
||||
memberId: string
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from('org_members').delete().eq('id', memberId);
|
||||
if (error) throw error;
|
||||
if (error) {
|
||||
log.error('removeMember failed', { error, data: { memberId } });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function generateSlug(name: string): string {
|
||||
|
||||
Reference in New Issue
Block a user