|
@@ -33,26 +33,8 @@ interface ShareLinkRecord {
|
|
|
created_at: string;
|
|
created_at: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Backend returns paginated response with { data: [...], meta: {...} }
|
|
|
|
|
-interface PaginatedResponse<T> {
|
|
|
|
|
- data: T[];
|
|
|
|
|
- meta?: {
|
|
|
|
|
- total: number;
|
|
|
|
|
- limit: number;
|
|
|
|
|
- offset: number;
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// Helper to extract array from paginated response
|
|
|
|
|
-function extractData<T>(response: { data: T[] | PaginatedResponse<T> | null }): T[] {
|
|
|
|
|
- if (!response.data) return [];
|
|
|
|
|
- // Check if response.data is the paginated wrapper or direct array
|
|
|
|
|
- if (Array.isArray(response.data)) {
|
|
|
|
|
- return response.data;
|
|
|
|
|
- }
|
|
|
|
|
- // It's a paginated response { data: [...], meta: {...} }
|
|
|
|
|
- return (response.data as PaginatedResponse<T>).data || [];
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// Note: the SDK's .get() already unwraps the server's { data, meta } envelope and
|
|
|
|
|
+// returns a plain array on response.data, so no manual extraction is needed.
|
|
|
|
|
|
|
|
function generateLinkCode(): string {
|
|
function generateLinkCode(): string {
|
|
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
@@ -131,39 +113,25 @@ export async function listShareLinks(
|
|
|
.select('*')
|
|
.select('*')
|
|
|
.eq('image_short_code', shortCode)
|
|
.eq('image_short_code', shortCode)
|
|
|
.order('created_at', { ascending: false })
|
|
.order('created_at', { ascending: false })
|
|
|
|
|
+ .limit(500) // explicit bound: the list endpoint silently caps at 100 otherwise
|
|
|
.get();
|
|
.get();
|
|
|
|
|
|
|
|
if (response.error) {
|
|
if (response.error) {
|
|
|
throw new Error(response.error.message || 'Failed to load share links');
|
|
throw new Error(response.error.message || 'Failed to load share links');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const records = extractData<ShareLinkRecord>(response);
|
|
|
|
|
- return records.map(recordToShareLink);
|
|
|
|
|
|
|
+ return (response.data ?? []).map(recordToShareLink);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export async function revokeShareLink(
|
|
export async function revokeShareLink(
|
|
|
client: BaaSClient,
|
|
client: BaaSClient,
|
|
|
linkCode: string
|
|
linkCode: string
|
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
|
- // First find the link by link_code
|
|
|
|
|
- const findResponse = await client
|
|
|
|
|
- .from<ShareLinkRecord>('share_links')
|
|
|
|
|
- .select('id')
|
|
|
|
|
- .eq('link_code', linkCode)
|
|
|
|
|
- .limit(1)
|
|
|
|
|
- .get();
|
|
|
|
|
-
|
|
|
|
|
- const records = extractData<ShareLinkRecord>(findResponse);
|
|
|
|
|
- if (findResponse.error || records.length === 0) {
|
|
|
|
|
- throw new Error('Share link not found');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const linkId = records[0].id;
|
|
|
|
|
-
|
|
|
|
|
- // Update the link to mark it as revoked
|
|
|
|
|
|
|
+ // Filter UPDATE by link_code in a single round-trip (server-side filter
|
|
|
|
|
+ // mutation), no find-then-update.
|
|
|
const updateResponse = await client
|
|
const updateResponse = await client
|
|
|
.from<ShareLinkRecord>('share_links')
|
|
.from<ShareLinkRecord>('share_links')
|
|
|
- .eq('id', linkId)
|
|
|
|
|
|
|
+ .eq('link_code', linkCode)
|
|
|
.update({ is_revoked: true });
|
|
.update({ is_revoked: true });
|
|
|
|
|
|
|
|
if (updateResponse.error) {
|
|
if (updateResponse.error) {
|
|
@@ -179,43 +147,36 @@ export async function getShareLinkByCode(
|
|
|
.from<ShareLinkRecord>('share_links')
|
|
.from<ShareLinkRecord>('share_links')
|
|
|
.select('*')
|
|
.select('*')
|
|
|
.eq('link_code', linkCode)
|
|
.eq('link_code', linkCode)
|
|
|
- .limit(1)
|
|
|
|
|
- .get();
|
|
|
|
|
|
|
+ .maybeSingle();
|
|
|
|
|
|
|
|
- const records = extractData<ShareLinkRecord>(response);
|
|
|
|
|
- if (response.error || records.length === 0) {
|
|
|
|
|
|
|
+ if (response.error || !response.data) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return recordToShareLink(records[0]);
|
|
|
|
|
|
|
+ return recordToShareLink(response.data);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export async function incrementViewCount(
|
|
export async function incrementViewCount(
|
|
|
client: BaaSClient,
|
|
client: BaaSClient,
|
|
|
linkCode: string
|
|
linkCode: string
|
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
|
- // Find the link
|
|
|
|
|
- const findResponse = await client
|
|
|
|
|
|
|
+ const response = await client
|
|
|
.from<ShareLinkRecord>('share_links')
|
|
.from<ShareLinkRecord>('share_links')
|
|
|
.select('id,view_count')
|
|
.select('id,view_count')
|
|
|
.eq('link_code', linkCode)
|
|
.eq('link_code', linkCode)
|
|
|
- .limit(1)
|
|
|
|
|
- .get();
|
|
|
|
|
|
|
+ .maybeSingle();
|
|
|
|
|
|
|
|
- const records = extractData<ShareLinkRecord>(findResponse);
|
|
|
|
|
- if (findResponse.error || records.length === 0) {
|
|
|
|
|
|
|
+ if (response.error || !response.data) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const link = records[0];
|
|
|
|
|
- const currentCount = link.view_count || 0;
|
|
|
|
|
-
|
|
|
|
|
- // Update view count
|
|
|
|
|
|
|
+ // Note: read-modify-write — a true atomic increment would need a server-side
|
|
|
|
|
+ // function; acceptable for a view counter. Updated by link_code in one call.
|
|
|
await client
|
|
await client
|
|
|
.from<ShareLinkRecord>('share_links')
|
|
.from<ShareLinkRecord>('share_links')
|
|
|
- .eq('id', link.id)
|
|
|
|
|
|
|
+ .eq('link_code', linkCode)
|
|
|
.update({
|
|
.update({
|
|
|
- view_count: currentCount + 1,
|
|
|
|
|
|
|
+ view_count: (response.data.view_count ?? 0) + 1,
|
|
|
last_viewed_at: new Date().toISOString(),
|
|
last_viewed_at: new Date().toISOString(),
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|