mono/packages/ui/src/components/ImageWizard/db.ts

411 lines
12 KiB
TypeScript

/**
* Database operations for ImageWizard
* All Supabase queries and mutations isolated here
*/
import { supabase } from "@/integrations/supabase/client";
import { toast } from "sonner";
import { translate } from "@/i18n";
/**
* Get organization ID by slug
*/
export const getOrganizationId = async (orgSlug: string): Promise<string | null> => {
try {
const { data: org } = await supabase
.from('organizations')
.select('id')
.eq('slug', orgSlug)
.single();
return org?.id || null;
} catch (error) {
console.error('Error fetching organization:', error);
return null;
}
};
/**
* Upload image blob to storage
*/
export const uploadImageToStorage = async (
userId: string,
blob: Blob,
suffix: string = 'generated'
): Promise<{ fileName: string; publicUrl: string } | null> => {
try {
const fileName = `${userId}/${Date.now()}-${suffix}.png`;
const { error: uploadError } = await supabase.storage
.from('pictures')
.upload(fileName, blob);
if (uploadError) throw uploadError;
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('pictures')
.getPublicUrl(fileName);
return { fileName, publicUrl };
} catch (error) {
console.error('Error uploading image to storage:', error);
throw error;
}
};
/**
* Create new picture in database
*/
export const createPictureRecord = async (params: {
userId: string;
title: string | null;
description: string | null;
imageUrl: string;
organizationId?: string | null;
parentId?: string | null;
isSelected?: boolean;
}): Promise<{ id: string } | null> => {
try {
const { data: pictureData, error: dbError } = await supabase
.from('pictures')
.insert([{
title: params.title?.trim() || null,
description: params.description || null,
image_url: params.imageUrl,
user_id: params.userId,
parent_id: params.parentId || null,
is_selected: params.isSelected ?? false,
organization_id: params.organizationId || null,
}])
.select()
.single();
if (dbError) throw dbError;
return pictureData;
} catch (error) {
console.error('Error creating picture record:', error);
throw error;
}
};
/**
* Add picture to collections
*/
export const addPictureToCollections = async (
pictureId: string,
collectionIds: string[]
): Promise<boolean> => {
try {
const collectionInserts = collectionIds.map(collectionId => ({
collection_id: collectionId,
picture_id: pictureId
}));
const { error: collectionError } = await supabase
.from('collection_pictures')
.insert(collectionInserts);
if (collectionError) {
console.error('Error adding to collections:', collectionError);
return false;
}
return true;
} catch (error) {
console.error('Error adding to collections:', error);
return false;
}
};
/**
* Unselect all images in a family (root and all versions)
*/
export const unselectImageFamily = async (
rootParentId: string,
userId: string
): Promise<void> => {
try {
await supabase
.from('pictures')
.update({ is_selected: false })
.or(`id.eq.${rootParentId},parent_id.eq.${rootParentId}`)
.eq('user_id', userId);
} catch (error) {
console.error('Error unselecting image family:', error);
throw error;
}
};
/**
* Check selection status of an image
*/
export const getImageSelectionStatus = async (imageId: string): Promise<boolean> => {
try {
const { data } = await supabase
.from('pictures')
.select('is_selected')
.eq('id', imageId)
.single();
return data?.is_selected || false;
} catch (error) {
console.error('Error getting image selection status:', error);
return false;
}
};
/**
* Publish image as new post
*/
export const publishImageAsNew = async (params: {
userId: string;
blob: Blob;
title: string;
description?: string;
isOrgContext: boolean;
orgSlug?: string;
collectionIds?: string[];
}): Promise<void> => {
const { userId, blob, title, description, isOrgContext, orgSlug, collectionIds } = params;
// Upload to storage
const uploadResult = await uploadImageToStorage(userId, blob, 'generated');
if (!uploadResult) throw new Error('Failed to upload image');
// Get organization ID if needed
let organizationId = null;
if (isOrgContext && orgSlug) {
organizationId = await getOrganizationId(orgSlug);
}
// Create picture record
const pictureData = await createPictureRecord({
userId,
title,
description: description || null,
imageUrl: uploadResult.publicUrl,
organizationId,
});
if (!pictureData) throw new Error('Failed to create picture record');
// Add to collections if specified
if (collectionIds && collectionIds.length > 0) {
const success = await addPictureToCollections(pictureData.id, collectionIds);
if (success) {
toast.success(translate(`Image published and added to ${collectionIds.length} collection(s)!`));
} else {
toast.error(translate('Image published but failed to add to collections'));
}
} else {
toast.success(translate('Image published to gallery!'));
}
};
/**
* Publish image as version of existing image
*/
export const publishImageAsVersion = async (params: {
userId: string;
blob: Blob;
title: string;
description?: string;
parentId: string;
isOrgContext: boolean;
orgSlug?: string;
collectionIds?: string[];
}): Promise<void> => {
const { userId, blob, title, description, parentId, isOrgContext, orgSlug, collectionIds } = params;
// Upload to storage
const uploadResult = await uploadImageToStorage(userId, blob, 'version');
if (!uploadResult) throw new Error('Failed to upload image');
// Unselect all images in the family first
const rootParentId = parentId && parentId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) ? parentId : null;
if (rootParentId) {
await unselectImageFamily(rootParentId, userId);
}
// Get organization ID if needed
let organizationId = null;
if (isOrgContext && orgSlug) {
organizationId = await getOrganizationId(orgSlug);
}
// Create version record (selected by default)
const pictureData = await createPictureRecord({
userId,
title,
description: description || null,
imageUrl: uploadResult.publicUrl,
organizationId,
parentId: rootParentId,
isSelected: true,
});
if (!pictureData) throw new Error('Failed to create version record');
// Add to collections if specified
if (collectionIds && collectionIds.length > 0) {
const success = await addPictureToCollections(pictureData.id, collectionIds);
if (success) {
toast.success(translate(`Version saved and added to ${collectionIds.length} collection(s)!`));
} else {
toast.error(translate('Version saved but failed to add to collections'));
}
} else {
toast.success(translate('Version saved successfully!'));
}
};
/**
* Publish image directly to an existing post
*/
export const publishImageToPost = async (params: {
userId: string;
blob: Blob;
title: string;
description?: string;
postId: string;
isOrgContext: boolean;
orgSlug?: string;
collectionIds?: string[];
}): Promise<void> => {
const { userId, blob, title, description, postId, isOrgContext, orgSlug, collectionIds } = params;
// Upload to storage
const uploadResult = await uploadImageToStorage(userId, blob, 'post-add');
if (!uploadResult) throw new Error('Failed to upload image');
// Get organization ID if needed
let organizationId = null;
if (isOrgContext && orgSlug) {
organizationId = await getOrganizationId(orgSlug);
}
// Get current max position for this post to append at the end
const { data: maxPosData } = await supabase
.from('pictures')
.select('position')
.eq('post_id', postId)
.order('position', { ascending: false })
.limit(1)
.single();
const nextPosition = (maxPosData?.position || 0) + 1;
// Create picture record attached to post
const { data: pictureData, error: dbError } = await supabase
.from('pictures')
.insert([{
title: title,
description: description || null,
image_url: uploadResult.publicUrl,
user_id: userId,
post_id: postId,
position: nextPosition,
is_selected: true,
organization_id: organizationId || null,
}])
.select()
.single();
if (dbError) throw dbError;
if (!pictureData) throw new Error('Failed to create picture record');
// Add to collections if specified
if (collectionIds && collectionIds.length > 0) {
const success = await addPictureToCollections(pictureData.id, collectionIds);
if (success) {
toast.success(translate(`Image added to post and ${collectionIds.length} collection(s)!`));
} else {
toast.error(translate('Image added to post but failed to add to collections'));
}
} else {
toast.success(translate('Image added to post successfully!'));
}
};
/**
* Get user's OpenAI API key from user_secrets.settings
*/
export const getUserOpenAIKey = async (userId: string): Promise<string | null> => {
try {
const secrets = await getUserSecrets(userId);
return secrets?.openai_api_key || null;
} catch (error) {
console.error('Error fetching OpenAI key:', error);
return null;
}
};
/**
* Get user secrets from user_secrets table (settings column)
*/
export const getUserSecrets = async (userId: string): Promise<Record<string, string> | null> => {
console.log('Fetching user secrets for user:', userId);
try {
const { data: secretData } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.single();
if (!secretData?.settings) return null;
const settings = secretData.settings as Record<string, any>;
return (settings.api_keys as Record<string, string>) || null;
} catch (error) {
console.error('Error fetching user secrets:', error);
return null;
}
};
/**
* Update user secrets in user_secrets table (settings column)
*/
export const updateUserSecrets = async (userId: string, secrets: Record<string, string>): Promise<void> => {
try {
// Check if record exists
const { data: existing } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (existing) {
// Update existing
const currentSettings = (existing.settings as Record<string, any>) || {};
const currentApiKeys = (currentSettings.api_keys as Record<string, any>) || {};
const newSettings = {
...currentSettings,
api_keys: { ...currentApiKeys, ...secrets }
};
const { error } = await supabase
.from('user_secrets')
.update({ settings: newSettings })
.eq('user_id', userId);
if (error) throw error;
} else {
// Insert new
const { error } = await supabase
.from('user_secrets')
.insert({
user_id: userId,
settings: { api_keys: secrets }
});
if (error) throw error;
}
} catch (error) {
console.error('Error updating user secrets:', error);
throw error;
}
};