411 lines
12 KiB
TypeScript
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;
|
|
}
|
|
};
|
|
|