-
Notifications
You must be signed in to change notification settings - Fork 256
Expand file tree
/
Copy pathfolderService.ts
More file actions
243 lines (202 loc) · 7.11 KB
/
Copy pathfolderService.ts
File metadata and controls
243 lines (202 loc) · 7.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/**
* Folder Service
*
* Business logic for page folder operations
*/
import { getSupabaseAdmin } from '@/lib/supabase-server';
import type { PageFolder } from '@/types';
/**
* Result of folder publishing operation
*/
export interface PublishFoldersResult {
count: number;
}
/**
* Collect ancestor folder IDs for given page folder IDs
* Traverses up the folder tree to ensure all parent folders are published
*/
async function collectAncestorFolderIds(
pageIds: string[],
client: any
): Promise<Set<string>> {
const folderIdsToPublish = new Set<string>();
// Fetch pages to get their folder IDs
const { data: pagesToPublish } = await client
.from('pages')
.select('page_folder_id')
.in('id', pageIds)
.eq('is_published', false)
.is('deleted_at', null);
if (!pagesToPublish) {
return folderIdsToPublish;
}
// Get all draft folders to traverse ancestors
const { data: allDraftFolders } = await client
.from('page_folders')
.select('*')
.eq('is_published', false)
.is('deleted_at', null);
if (!allDraftFolders) {
return folderIdsToPublish;
}
const foldersById = new Map<string, PageFolder>(
allDraftFolders.map((f: PageFolder) => [f.id, f])
);
// Collect all ancestor folder IDs
const collectAncestors = (folderId: string | null): void => {
if (!folderId) return;
const folder = foldersById.get(folderId);
if (folder) {
folderIdsToPublish.add(folder.id);
collectAncestors(folder.page_folder_id);
}
};
for (const page of pagesToPublish) {
if (page.page_folder_id) {
collectAncestors(page.page_folder_id);
}
}
return folderIdsToPublish;
}
/**
* Publish folders by their IDs
* Handles soft-deleted folders and sorts by depth to ensure parents are published first
*
* @param folderIds - Array of folder IDs to publish (empty array = publish all)
* @param pageIds - Optional array of page IDs to collect ancestor folders from
* @returns Number of folders published
*/
export async function publishFolders(
folderIds: string[] = [],
pageIds?: string[]
): Promise<PublishFoldersResult> {
const client = await getSupabaseAdmin();
if (!client) {
throw new Error('Supabase not configured');
}
const isPublishingAll = folderIds.length === 0;
const folderIdsToPublish = new Set<string>(folderIds);
// Collect ancestor folders if page IDs provided
if (!isPublishingAll && pageIds && pageIds.length > 0) {
const ancestorIds = await collectAncestorFolderIds(pageIds, client);
ancestorIds.forEach(id => folderIdsToPublish.add(id));
}
// Skip if no folders to publish
if (!isPublishingAll && folderIdsToPublish.size === 0) {
return { count: 0 };
}
// Get all draft folders (including soft-deleted for cleanup)
const { data: allDraftFolders, error: foldersError } = await client
.from('page_folders')
.select('*')
.eq('is_published', false);
if (foldersError || !allDraftFolders) {
throw new Error(`Failed to fetch folders: ${foldersError?.message}`);
}
// Filter folders based on request
const foldersToProcess = isPublishingAll
? allDraftFolders
: allDraftFolders.filter((f: PageFolder) => folderIdsToPublish.has(f.id));
// Separate active and soft-deleted folders
const activeFolders = foldersToProcess.filter((f: PageFolder) => f.deleted_at === null);
const softDeletedFolders = foldersToProcess.filter((f: PageFolder) => f.deleted_at !== null);
// Get existing published folders (need full data to verify parent relationships)
const folderIdsToCheck = foldersToProcess.map((f: PageFolder) => f.id);
// Also get all parent folder IDs that we might need to reference
const parentFolderIds = new Set<string>();
foldersToProcess.forEach((f: PageFolder) => {
if (f.page_folder_id) {
parentFolderIds.add(f.page_folder_id);
}
});
const allIdsToCheck = [...new Set([...folderIdsToCheck, ...parentFolderIds])];
// Fetch all published folders we need to reference
const { data: existingPublished } = await client
.from('page_folders')
.select('*')
.eq('is_published', true)
.in('id', allIdsToCheck);
const publishedFoldersById = new Map<string, PageFolder>(
(existingPublished || []).map((f: PageFolder) => [f.id, f])
);
const publishedIds = new Set(publishedFoldersById.keys());
if (folderIdsToCheck.length > 0) {
// Soft-delete published versions of soft-deleted drafts
const idsToSoftDelete = softDeletedFolders
.filter((f: PageFolder) => publishedIds.has(f.id))
.map((f: PageFolder) => f.id);
if (idsToSoftDelete.length > 0) {
await client
.from('page_folders')
.update({ deleted_at: new Date().toISOString() })
.eq('is_published', true)
.in('id', idsToSoftDelete)
.is('deleted_at', null);
}
}
// Sort active folders by depth (parents first)
const sortedFolders = [...activeFolders].sort(
(a: PageFolder, b: PageFolder) => (a.depth || 0) - (b.depth || 0)
);
// Track folders being published in this batch
const foldersBeingPublished = new Set<string>();
// Prepare folders to upsert, resolving parent folder IDs to published versions
const foldersToUpsert: Array<{
id: string;
name: string;
slug: string;
page_folder_id: string | null;
order: number | null;
depth: number;
settings: PageFolder['settings'];
is_published: boolean;
}> = [];
for (const folder of sortedFolders) {
let publishedParentId: string | null = null;
if (folder.page_folder_id) {
// Check if parent is already published or being published in this batch
const parentIsPublished = publishedFoldersById.has(folder.page_folder_id);
const parentIsInBatch = foldersBeingPublished.has(folder.page_folder_id);
if (!parentIsPublished && !parentIsInBatch) {
// Parent folder is not published and not in this batch - skip this folder
continue;
}
// Use the same ID since published folders share the same ID as drafts
publishedParentId = folder.page_folder_id;
}
foldersBeingPublished.add(folder.id);
// Skip if published version exists and is identical
const existing = publishedFoldersById.get(folder.id);
if (
existing &&
existing.name === folder.name &&
existing.slug === folder.slug &&
existing.page_folder_id === publishedParentId &&
existing.order === folder.order &&
existing.depth === folder.depth &&
JSON.stringify(existing.settings) === JSON.stringify(folder.settings)
) {
continue;
}
foldersToUpsert.push({
id: folder.id,
name: folder.name,
slug: folder.slug,
page_folder_id: publishedParentId,
order: folder.order,
depth: folder.depth,
settings: folder.settings,
is_published: true,
});
}
if (foldersToUpsert.length === 0) {
return { count: 0 };
}
const { error: upsertError } = await client
.from('page_folders')
.upsert(foldersToUpsert, { onConflict: 'id,is_published' });
if (upsertError) {
throw new Error(`Failed to publish folders: ${upsertError.message}`);
}
return { count: foldersToUpsert.length };
}