@nextblock-cms/db 0.2.32 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/cache-Bb4TkKGq.js +38 -0
  2. package/cache-D0Q6YeSV.mjs +10150 -0
  3. package/index.cjs.js +1 -1
  4. package/index.es.js +66 -61
  5. package/package.json +1 -1
  6. package/server.cjs.js +1 -1
  7. package/server.es.js +48 -43
  8. package/supabase/config.toml +330 -319
  9. package/supabase/functions/import_map.json +6 -0
  10. package/supabase/functions/nextblock-migration-ingest/index.d.ts +1 -0
  11. package/supabase/functions/nextblock-migration-ingest/index.js +519 -0
  12. package/supabase/functions/nextblock-migration-ingest/index.js.map +7 -0
  13. package/supabase/functions/nextblock-migration-ingest/index.ts +646 -0
  14. package/supabase/migrations/00000000000000_setup_foundation_and_enums.sql +40 -0
  15. package/supabase/migrations/00000000000001_setup_cms_core.sql +124 -0
  16. package/supabase/migrations/00000000000002_setup_content_tables.sql +138 -0
  17. package/supabase/migrations/00000000000003_setup_catalog_and_licensing.sql +160 -0
  18. package/supabase/migrations/00000000000004_setup_fulfillment_shipping_taxes_and_currencies.sql +797 -0
  19. package/supabase/migrations/00000000000005_setup_functions_and_triggers.sql +767 -0
  20. package/supabase/migrations/00000000000006_setup_rls_and_grants.sql +872 -0
  21. package/supabase/migrations/00000000000007_setup_indexes.sql +158 -0
  22. package/supabase/migrations/00000000000008_seed_platform_defaults.sql +155 -0
  23. package/supabase/migrations/00000000000009_seed_translations.sql +801 -0
  24. package/supabase/migrations/00000000000010_seed_content_scaffold.sql +1088 -0
  25. package/supabase/migrations/00000000000011_setup_cortex_ai_settings.sql +81 -0
  26. package/supabase/migrations/00000000000012_setup_commerce_coupons.sql +275 -0
  27. package/supabase/migrations/00000000000013_setup_cortex_ai_db_mutation_audit.sql +47 -0
  28. package/supabase/migrations/00000000000014_setup_content_drafts.sql +119 -0
  29. package/supabase/migrations/00000000000015_setup_product_drafts.sql +97 -0
  30. package/supabase/migrations/00000000000016_add_feature_image_to_pages.sql +8 -0
  31. package/supabase/migrations/00000000000017_add_product_blocks.sql +118 -0
  32. package/supabase/migrations/00000000000018_setup_bot_protection_settings.sql +77 -0
  33. package/supabase/migrations/00000000000019_add_product_categories.sql +64 -0
  34. package/supabase/migrations/00000000000020_add_category_translations.sql +10 -0
  35. package/supabase/migrations/00000000000021_migrate_hero_blocks_to_sections.sql +29 -0
  36. package/supabase/migrations/00000000000022_seed_cortex_ai_guide_post.sql +184 -0
  37. package/supabase/migrations/00000000000023_setup_custom_block_definitions.sql +183 -0
  38. package/supabase/migrations/00000000000024_setup_ucp_cart_sessions.sql +72 -0
  39. package/supabase/migrations/00000000000025_add_sale_schedule_columns.sql +406 -0
  40. package/supabase/migrations/00000000000026_seed_on_sale_translation.sql +9 -0
  41. package/supabase/migrations/00000000000027_setup_privacy_and_mfa.sql +479 -0
  42. package/supabase/migrations/00000000000028_clear_advisor_warnings.sql +93 -0
  43. package/supabase/migrations/00000000000029_refresh_setup_article.sql +252 -0
  44. package/supabase/templates/confirmation.html +54 -0
  45. package/supabase/templates/email_change.html +53 -0
  46. package/supabase/templates/invite.html +53 -0
  47. package/supabase/templates/magic_link.html +53 -0
  48. package/supabase/templates/reauthentication.html +45 -0
  49. package/supabase/templates/recovery.html +54 -0
  50. package/index.d.ts +0 -4
  51. package/lib/supabase/client.d.ts +0 -9
  52. package/lib/supabase/middleware.d.ts +0 -2
  53. package/lib/supabase/server.d.ts +0 -7
  54. package/lib/supabase/ssg-client.d.ts +0 -2
  55. package/lib/supabase/types.d.ts +0 -635
  56. package/server.d.ts +0 -2
  57. package/ssg-client-CiaENGEv.js +0 -1
  58. package/ssg-client-DgUYZgPq.mjs +0 -8
  59. package/supabase/migrations/00000000000000_setup_extensions_and_roles.sql +0 -54
  60. package/supabase/migrations/00000000000001_setup_site_settings.sql +0 -19
  61. package/supabase/migrations/00000000000002_setup_profiles.sql +0 -87
  62. package/supabase/migrations/00000000000003_setup_languages.sql +0 -43
  63. package/supabase/migrations/00000000000004_setup_media.sql +0 -54
  64. package/supabase/migrations/00000000000005_setup_posts.sql +0 -56
  65. package/supabase/migrations/00000000000006_setup_pages.sql +0 -51
  66. package/supabase/migrations/00000000000007_setup_blocks.sql +0 -47
  67. package/supabase/migrations/00000000000008_setup_navigation.sql +0 -51
  68. package/supabase/migrations/00000000000009_setup_logos.sql +0 -13
  69. package/supabase/migrations/00000000000010_setup_translations.sql +0 -29
  70. package/supabase/migrations/00000000000011_setup_revisions.sql +0 -38
  71. package/supabase/migrations/00000000000012_setup_rls_policies.sql +0 -239
  72. package/supabase/migrations/00000000000013_seed_data.sql +0 -149
  73. package/supabase/migrations/00000000000014_seed_homepage_blocks.sql +0 -661
  74. package/supabase/migrations/00000000000015_seed_how_it_works.sql +0 -89
  75. package/supabase/migrations/00000000000016_sandbox_setup.sql +0 -834
@@ -0,0 +1,646 @@
1
+ /// <reference types="deno" />
2
+ import { createClient } from "@supabase/supabase-js";
3
+ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
4
+
5
+ const corsHeaders = {
6
+ 'Access-Control-Allow-Origin': '*',
7
+ 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8
+ };
9
+
10
+ // Upload S3 / R2 Proxy Handler
11
+ async function processMediaUpload(url: string, r2Config: any, warnings: string[]): Promise<{ publicUrl: string; objectKey: string; contentType: string } | null> {
12
+ if (!r2Config || !r2Config.account_id) {
13
+ warnings.push(`R2 config missing for ${url}`);
14
+ return null;
15
+ }
16
+
17
+ try {
18
+ let arrayBuffer: ArrayBuffer;
19
+ let contentType: string;
20
+ let filename: string;
21
+
22
+ if (url.startsWith('data:')) {
23
+ const commaIdx = url.indexOf(',');
24
+ const header = url.substring(0, commaIdx);
25
+ const base64Data = url.substring(commaIdx + 1);
26
+ const mimeMatch = header.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+)/);
27
+ contentType = mimeMatch ? mimeMatch[1] : 'application/octet-stream';
28
+
29
+ const binaryString = atob(base64Data);
30
+ const len = binaryString.length;
31
+ const bytes = new Uint8Array(len);
32
+ for (let i = 0; i < len; i++) {
33
+ bytes[i] = binaryString.charCodeAt(i);
34
+ }
35
+ arrayBuffer = bytes.buffer;
36
+
37
+ const extension = contentType.split('/')[1] || 'jpg';
38
+ filename = `upload_${crypto.randomUUID()}.${extension}`;
39
+ } else {
40
+ const controller = new AbortController();
41
+ const timeoutId = setTimeout(() => controller.abort(), 6000); // 6-second timeout
42
+ let fileResponse;
43
+ try {
44
+ fileResponse = await fetch(url, { signal: controller.signal });
45
+ clearTimeout(timeoutId);
46
+ } catch (err: any) {
47
+ clearTimeout(timeoutId);
48
+ console.error(`Fetch aborted/failed for ${url}:`, err.message || err);
49
+ warnings.push(`Fetch aborted/failed for ${url}: ${err.message || 'Timeout'}`);
50
+ return null;
51
+ }
52
+ if (!fileResponse.ok) {
53
+ warnings.push(`Fetch failed for ${url}: HTTP ${fileResponse.status}`);
54
+ return null;
55
+ }
56
+ arrayBuffer = await fileResponse.arrayBuffer();
57
+ contentType = fileResponse.headers.get('content-type') || 'application/octet-stream';
58
+ filename = url.split('/').pop() || crypto.randomUUID();
59
+ }
60
+
61
+ const s3Client = new S3Client({
62
+ region: "auto",
63
+ endpoint: `https://${r2Config.account_id}.r2.cloudflarestorage.com`,
64
+ credentials: {
65
+ accessKeyId: r2Config.access_key,
66
+ secretAccessKey: r2Config.secret_key,
67
+ },
68
+ });
69
+
70
+ const extension = filename.split('.').pop() || 'jpg';
71
+ const objectKey = `migrated/${crypto.randomUUID()}.${extension}`;
72
+
73
+ await s3Client.send(new PutObjectCommand({
74
+ Bucket: r2Config.bucket,
75
+ Key: objectKey,
76
+ Body: new Uint8Array(arrayBuffer),
77
+ ContentType: contentType,
78
+ }));
79
+
80
+ const publicUrl = `${r2Config.public_domain.replace(/\/$/, '')}/${objectKey}`;
81
+ return { publicUrl, objectKey, contentType };
82
+ } catch(e: any) {
83
+ console.error(`R2 Upload Failed for ${url}:`, e);
84
+ warnings.push(`R2 Upload Failed for ${url}: ${e.message}`);
85
+ return null;
86
+ }
87
+ }
88
+
89
+ Deno.serve(async (req: Request) => {
90
+ if (req.method === 'OPTIONS') {
91
+ return new Response('ok', { headers: corsHeaders });
92
+ }
93
+
94
+ try {
95
+ const supabaseClient = createClient(
96
+ Deno.env.get('SUPABASE_URL') ?? '',
97
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
98
+ );
99
+
100
+ const body = await req.json();
101
+ const { entity_type, r2_config, data, wipe_seeds } = body;
102
+
103
+ if (!data || !Array.isArray(data)) {
104
+ return new Response(JSON.stringify({ error: 'Payload data must be a structured array' }), {
105
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
106
+ status: 400,
107
+ });
108
+ }
109
+
110
+ const warnings: string[] = [];
111
+
112
+ // Erase existing seeded content if requested before importing the new ones
113
+ if (wipe_seeds && data.length > 0) {
114
+ if (entity_type === 'page' || entity_type === 'post') {
115
+ await supabaseClient.from(entity_type === 'post' ? 'posts' : 'pages').delete().neq('id', 0);
116
+ } else if (entity_type === 'product' || entity_type === 'product_variation') {
117
+ await supabaseClient.from('products').delete().neq('id', '00000000-0000-0000-0000-000000000000');
118
+ }
119
+ }
120
+
121
+ const normalizeLanguageCode = (code: unknown): string => {
122
+ const normalized = String(code || '').trim().toLowerCase().replace('_', '-');
123
+ return normalized.split('-')[0] || 'en';
124
+ };
125
+
126
+ const { data: languages, error: langError } = await supabaseClient.from('languages').select('id, code');
127
+ if (langError) throw langError;
128
+ const langMap = new Map();
129
+ if (languages) {
130
+ languages.forEach((l: any) => {
131
+ langMap.set(normalizeLanguageCode(l.code), l.id);
132
+ langMap.set(String(l.code || '').toLowerCase(), l.id);
133
+ });
134
+ }
135
+
136
+ const { data: allUsers } = await supabaseClient.auth.admin.listUsers();
137
+ const userEmailMap = new Map();
138
+ if (allUsers?.users) {
139
+ allUsers.users.forEach((u: any) => userEmailMap.set(u.email, u.id));
140
+ }
141
+
142
+ const processedEntities: any[] = [];
143
+ let result: any = { error: null };
144
+
145
+ // GLOBAL CUSTOMIZER CSS
146
+ if (data.length > 0 && data[0].global_customizer_css) {
147
+ await supabaseClient.from('site_settings').upsert({
148
+ key: 'global_css',
149
+ value: data[0].global_customizer_css
150
+ }, { onConflict: 'key' });
151
+ }
152
+
153
+ // SITE LOGO MIGRATION
154
+ if (entity_type === 'site_logo') {
155
+ for (const item of data) {
156
+ if (!item.url) continue;
157
+
158
+ // Upload logo to R2
159
+ let logoMediaId = null;
160
+ if (r2_config?.bucket) {
161
+ const uploaded = await processMediaUpload(item.url, r2_config, warnings);
162
+ if (uploaded) {
163
+ const { data: mediaRec } = await supabaseClient.from('media').insert({
164
+ file_name: item.file_name || uploaded.objectKey.split('/').pop(),
165
+ object_key: uploaded.objectKey,
166
+ file_path: uploaded.publicUrl,
167
+ file_type: uploaded.contentType,
168
+ width: item.width || null,
169
+ height: item.height || null,
170
+ }).select('id').single();
171
+ if (mediaRec) logoMediaId = mediaRec.id;
172
+ }
173
+ }
174
+
175
+ if (logoMediaId) {
176
+ // Check if a logo already exists and update it, or insert a new one
177
+ const { data: existingLogo } = await supabaseClient
178
+ .from('logos')
179
+ .select('id')
180
+ .limit(1)
181
+ .maybeSingle();
182
+
183
+ if (existingLogo) {
184
+ await supabaseClient
185
+ .from('logos')
186
+ .update({ media_id: logoMediaId, name: 'Site Logo' })
187
+ .eq('id', existingLogo.id);
188
+ } else {
189
+ await supabaseClient.from('logos').insert({
190
+ name: 'Site Logo',
191
+ media_id: logoMediaId,
192
+ });
193
+ }
194
+ }
195
+
196
+ processedEntities.push({ logo: true, media_id: logoMediaId });
197
+ }
198
+
199
+ // USER MIGRATION LOOP
200
+ } else if (entity_type === 'user') {
201
+ for (const item of data) {
202
+ const randomPassword = crypto.randomUUID() + '!X9M';
203
+ const { data: userData, error: authError } = await supabaseClient.auth.admin.createUser({
204
+ email: item.email,
205
+ password: randomPassword,
206
+ email_confirm: true,
207
+ user_metadata: {
208
+ full_name: item.full_name || item.login,
209
+ stripe_customer_id: item.stripe_customer_id, // Preserving vault tokens
210
+ migrated_from_wp: true
211
+ }
212
+ });
213
+
214
+ if (authError || !userData?.user) {
215
+ console.error('Failed migrating user:', item.email, authError?.message);
216
+ warnings.push(`User ${item.email} migration failed: ${authError?.message}`);
217
+ continue;
218
+ }
219
+
220
+ if (item.addresses && item.addresses.length > 0) {
221
+ const mappedAddresses = item.addresses.map((addr: any) => ({
222
+ user_id: userData.user.id,
223
+ ...addr
224
+ }));
225
+ await supabaseClient.from('user_addresses').insert(mappedAddresses);
226
+ }
227
+ processedEntities.push({ user_id: userData.user.id });
228
+ }
229
+
230
+ // ORDER LEDGER MIGRATION LOOP
231
+ } else if (entity_type === 'shop_order') {
232
+ // Pre-fetch all SKUs needed for this chunk to eliminate sequential DB latency
233
+ const allSkusToFetch = new Set();
234
+ for (const item of data) {
235
+ if (item.order_items) {
236
+ for (const oi of item.order_items) {
237
+ if (oi.sku) allSkusToFetch.add(oi.sku);
238
+ }
239
+ }
240
+ }
241
+
242
+ const skuToProductId = new Map();
243
+ if (allSkusToFetch.size > 0) {
244
+ const { data: matchedProducts } = await supabaseClient
245
+ .from('products')
246
+ .select('id, sku')
247
+ .in('sku', Array.from(allSkusToFetch));
248
+
249
+ if (matchedProducts) {
250
+ for (const p of matchedProducts) {
251
+ skuToProductId.set(p.sku, p.id);
252
+ }
253
+ }
254
+ }
255
+
256
+ for (const item of data) {
257
+ const userId = item.user_email ? userEmailMap.get(item.user_email) || null : null;
258
+
259
+ const orderData = {
260
+ user_id: userId,
261
+ status: item.status,
262
+ total: item.total,
263
+ stripe_session_id: item.stripe_session_id,
264
+ payment_intent_id: item.payment_intent_id,
265
+ provider: item.provider,
266
+ created_at: item.created_at
267
+ };
268
+
269
+ // Deduplication mechanism for rerun idempotency
270
+ let insertedOrder = null;
271
+ let query = supabaseClient
272
+ .from('orders')
273
+ .select('id')
274
+ .eq('created_at', item.created_at)
275
+ .eq('total', item.total);
276
+
277
+ if (userId) {
278
+ query = query.eq('user_id', userId);
279
+ } else {
280
+ query = query.is('user_id', null);
281
+ }
282
+
283
+ const { data: existingMatch } = await query.maybeSingle();
284
+
285
+ if (existingMatch) {
286
+ insertedOrder = existingMatch;
287
+ } else {
288
+ const { data: newOrder, error: orderErr } = await supabaseClient.from('orders').insert(orderData).select().single();
289
+ if (orderErr || !newOrder) {
290
+ console.error("Failed to map order", orderErr);
291
+ warnings.push(`Order map failed: ${orderErr?.message || 'Unknown error'}`);
292
+ continue;
293
+ }
294
+ insertedOrder = newOrder;
295
+ }
296
+
297
+ if (item.order_items && item.order_items.length > 0 && !existingMatch) {
298
+ const mappedOrderItems = [];
299
+ for (const oi of item.order_items) {
300
+ // Resolve Product UUID instantly from pre-fetched hash map!
301
+ const pId = skuToProductId.get(oi.sku);
302
+ if (pId) {
303
+ mappedOrderItems.push({
304
+ order_id: insertedOrder.id,
305
+ product_id: pId,
306
+ quantity: oi.quantity,
307
+ price_at_purchase: oi.price_at_purchase
308
+ });
309
+ }
310
+ }
311
+ if (mappedOrderItems.length > 0) {
312
+ await supabaseClient.from('order_items').insert(mappedOrderItems);
313
+ }
314
+ }
315
+ processedEntities.push({ order_id: insertedOrder.id });
316
+ }
317
+
318
+ // NAVIGATION MENU LOOP
319
+ } else if (entity_type === 'nav_menu') {
320
+ type MenuKey = 'HEADER' | 'FOOTER' | 'SIDEBAR';
321
+ const validMenuKeys: MenuKey[] = ['HEADER', 'FOOTER', 'SIDEBAR'];
322
+ const selectedMenus = new Map<string, { item: any; menuKey: MenuKey; languageCode: string; languageId: number }>();
323
+
324
+ const resolveMenuKey = (item: any): MenuKey => {
325
+ const rawKey = String(item.nextblock_key || item.menu_key || item.slug || '').toUpperCase();
326
+ if (validMenuKeys.includes(rawKey as MenuKey)) {
327
+ return rawKey as MenuKey;
328
+ }
329
+
330
+ const location = String(item.location || '').toLowerCase();
331
+ if (location.includes('footer')) return 'FOOTER';
332
+ if (location.includes('sidebar')) return 'SIDEBAR';
333
+ return 'HEADER';
334
+ };
335
+
336
+ const resolveLanguageId = (languageCode: string): number => {
337
+ return langMap.get(languageCode) || langMap.get('en') || 1;
338
+ };
339
+
340
+ for (const item of data) {
341
+ const menuKey = resolveMenuKey(item);
342
+ const languageCode = normalizeLanguageCode(item.translation?.iso_code || item.language_code || item.locale || 'en');
343
+ const selectedKey = `${menuKey}:${languageCode}`;
344
+
345
+ if (selectedMenus.has(selectedKey)) {
346
+ const kept = selectedMenus.get(selectedKey);
347
+ warnings.push(
348
+ `Skipped duplicate ${menuKey}/${languageCode} menu "${item.name || item.id || 'unknown'}"; keeping "${kept?.item?.name || kept?.item?.id || 'first selected menu'}".`
349
+ );
350
+ continue;
351
+ }
352
+ selectedMenus.set(selectedKey, {
353
+ item,
354
+ menuKey,
355
+ languageCode,
356
+ languageId: resolveLanguageId(languageCode),
357
+ });
358
+ }
359
+
360
+ const menuKeysToClear = new Set<MenuKey>();
361
+ for (const selected of selectedMenus.values()) {
362
+ menuKeysToClear.add(selected.menuKey);
363
+ }
364
+
365
+ for (const menuKey of menuKeysToClear) {
366
+ await supabaseClient.from('navigation_items').delete().eq('menu_key', menuKey);
367
+ }
368
+
369
+ for (const { item, menuKey, languageCode, languageId } of selectedMenus.values()) {
370
+
371
+ const insertMenuNode = async (node: any, parent_id: number | null, orderIdx: number) => {
372
+ const navigationItem: any = {
373
+ language_id: languageId,
374
+ menu_key: menuKey,
375
+ label: node.title,
376
+ url: node.url,
377
+ parent_id: parent_id,
378
+ order: orderIdx
379
+ };
380
+
381
+ if (node.translation_group_id) {
382
+ navigationItem.translation_group_id = node.translation_group_id;
383
+ }
384
+
385
+ const { data: inserted, error } = await supabaseClient.from('navigation_items').insert(navigationItem).select('id').single();
386
+
387
+ if (!error && inserted && node.children) {
388
+ let childOrder = 0;
389
+ for (const child of node.children) {
390
+ await insertMenuNode(child, inserted.id, childOrder++);
391
+ }
392
+ }
393
+ };
394
+
395
+ let rootOrder = 0;
396
+ for (const rootNode of item.tree) {
397
+ await insertMenuNode(rootNode, null, rootOrder++);
398
+ }
399
+
400
+ processedEntities.push({ menu: menuKey, language: languageCode, source_menu_id: item.id, source_menu_name: item.name });
401
+ }
402
+
403
+ // STANDARD POST/PRODUCT CONTENT STREAM
404
+ } else {
405
+ for (const item of data) {
406
+ const language_id = langMap.get(item.translation?.iso_code) || langMap.get('en');
407
+
408
+ // R2 Inline HTML Image Swap Engine — matches both https:// URLs AND data: base64 URIs
409
+ let content_html = item.content_html || null;
410
+ if (content_html) {
411
+ // Strip srcset and sizes attributes to prevent loading broken WP URLs
412
+ content_html = content_html.replace(/\s*srcset=["']([^"']+)["']/gi, '');
413
+ content_html = content_html.replace(/\s*sizes=["']([^"']+)["']/gi, '');
414
+
415
+ if (r2_config?.bucket) {
416
+ // Match src="..." or src='...'
417
+ const matches = content_html.match(/src=["']([^"']+)["']/gi) || [];
418
+ if (matches.length > 0) {
419
+ for (const match of matches) {
420
+ const urlMatch = match.match(/src=["']([^"']+)["']/i);
421
+ if (urlMatch && urlMatch[1]) {
422
+ const url = urlMatch[1];
423
+ const isHttpImage = /^https?:\/\/[^'"]+\.(jpe?g|png|gif|webp|svg|bmp|avif)(\?.*)?$/i.test(url);
424
+ const isBase64Image = /^data:image\/[^'"]+$/i.test(url);
425
+ if (isHttpImage || isBase64Image) {
426
+ const uploaded = await processMediaUpload(url, r2_config, warnings);
427
+ if (uploaded) {
428
+ content_html = content_html.split(url).join(uploaded.publicUrl);
429
+ }
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+ item.content_html = content_html;
436
+ }
437
+
438
+ // Featured Image Extraction & public.media mapping
439
+ let feature_image_id = null;
440
+ if (item.featured_image && r2_config?.bucket) {
441
+ const uploaded = await processMediaUpload(item.featured_image, r2_config, warnings);
442
+ if (uploaded) {
443
+ const { data: mediaRec } = await supabaseClient.from('media').insert({
444
+ file_name: uploaded.objectKey.split('/').pop() || `migrated-${crypto.randomUUID()}.jpg`,
445
+ object_key: uploaded.objectKey,
446
+ file_path: uploaded.publicUrl,
447
+ file_type: uploaded.contentType,
448
+ }).select('id').single();
449
+ if (mediaRec) feature_image_id = mediaRec.id;
450
+ }
451
+ }
452
+
453
+ const author_id = item.author_email ? userEmailMap.get(item.author_email) || null : null;
454
+
455
+ if (entity_type === 'post' || entity_type === 'page') {
456
+ const baseData: any = {
457
+ language_id: language_id,
458
+ translation_group_id: item.translation?.translation_group_id,
459
+ title: item.title,
460
+ slug: item.slug,
461
+ status: item.status,
462
+ author_id: author_id,
463
+ meta_title: item.seo?.meta_title || null,
464
+ meta_description: item.seo?.meta_description || null,
465
+ feature_image_id,
466
+ created_at: item.created_at,
467
+ updated_at: item.updated_at,
468
+ };
469
+
470
+ // Posts specific fields
471
+ if (entity_type === 'post') {
472
+ baseData.excerpt = item.excerpt;
473
+ baseData.published_at = item.published_at;
474
+ }
475
+
476
+ processedEntities.push(baseData);
477
+
478
+ } else if (entity_type === 'product' || entity_type === 'product_variation') {
479
+ const ecom = item.ecommerce;
480
+ processedEntities.push({
481
+ language_id: language_id,
482
+ translation_group_id: item.translation?.translation_group_id,
483
+ title: item.title,
484
+ slug: item.slug,
485
+ sku: ecom?.sku || `wp-sku-${item.id}`,
486
+ product_type: 'physical',
487
+ payment_provider: 'stripe',
488
+ price: ecom?.price ? Math.round(parseFloat(ecom.price) * 100) : 0,
489
+ sale_price: ecom?.sale_price ? Math.round(parseFloat(ecom.sale_price) * 100) : null,
490
+ stock: ecom?.stock_qty || 0,
491
+ status: ecom?.stock_status === 'instock' ? 'active' : 'draft',
492
+ short_description: item.excerpt ? item.excerpt.replace(/<script\b[^>]*>(.*?)<\/script>/is, '') : null,
493
+ description_json: content_html ? content_html.replace(/<script\b[^>]*>(.*?)<\/script>/gi, '') : null,
494
+ meta_title: item.seo?.meta_title || null,
495
+ meta_description: item.seo?.meta_description || null,
496
+ metadata: item.custom_meta,
497
+ is_taxable: true,
498
+ created_at: item.created_at,
499
+ updated_at: item.updated_at,
500
+ _feature_image_id: feature_image_id,
501
+ });
502
+ }
503
+ }
504
+
505
+ if (entity_type === 'post' || entity_type === 'page') {
506
+ const tableName = entity_type === 'post' ? 'posts' : 'pages';
507
+
508
+ // Deduplicate by language_id + slug to prevent ON CONFLICT DO UPDATE error
509
+ const uniqueEntitiesMap = new Map();
510
+ for (const entity of processedEntities) {
511
+ // If a slug is empty, fallback to the post ID as slug to avoid empty slug collisions
512
+ if (!entity.slug) {
513
+ entity.slug = `untitled-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
514
+ }
515
+ const uniqueKey = `${entity.language_id}_${entity.slug}`;
516
+ uniqueEntitiesMap.set(uniqueKey, entity);
517
+ }
518
+ const finalEntitiesToUpsert = Array.from(uniqueEntitiesMap.values());
519
+
520
+ const { data: upsertedData, error: upsertErr } = await supabaseClient
521
+ .from(tableName)
522
+ .upsert(finalEntitiesToUpsert, { onConflict: 'language_id, slug' })
523
+ .select('id, slug, language_id');
524
+
525
+ if (upsertErr) {
526
+ console.error(`Migration Failed for ${tableName}:`, upsertErr);
527
+ throw upsertErr;
528
+ }
529
+
530
+ if (upsertedData) {
531
+ const blocksToInsert = [];
532
+ const upsertedIds = upsertedData.map((u: any) => u.id);
533
+ if (upsertedIds.length > 0) {
534
+ // Clean up old blocks for these specific records before re-inserting
535
+ await supabaseClient.from('blocks').delete().in(entity_type === 'post' ? 'post_id' : 'page_id', upsertedIds);
536
+ }
537
+
538
+ for (const item of data) {
539
+ const langId = langMap.get(item.translation?.iso_code) || langMap.get('en');
540
+ const dbRec = upsertedData.find((u: any) => u.slug === item.slug && u.language_id === langId);
541
+
542
+ if (!dbRec) continue;
543
+
544
+ let blockOrder = 0;
545
+
546
+ // Auto-inject product_grid block for WooCommerce shop pages
547
+ if (item.is_shop_page && entity_type === 'page') {
548
+ blocksToInsert.push({
549
+ page_id: dbRec.id,
550
+ language_id: langId,
551
+ block_type: 'product_grid',
552
+ content: { type: 'latest', limit: 12, title: '' },
553
+ order: blockOrder++
554
+ });
555
+ }
556
+
557
+ // Add the HTML content block if present
558
+ if (item.content_html) {
559
+ blocksToInsert.push({
560
+ [entity_type === 'post' ? 'post_id' : 'page_id']: dbRec.id,
561
+ language_id: langId,
562
+ block_type: 'text',
563
+ content: { html_content: item.content_html },
564
+ order: blockOrder++
565
+ });
566
+ }
567
+ }
568
+
569
+ if (blocksToInsert.length > 0) {
570
+ await supabaseClient.from('blocks').insert(blocksToInsert);
571
+ }
572
+ }
573
+ } else if (entity_type === 'product' || entity_type === 'product_variation') {
574
+ // Deduplicate by language_id + slug to prevent ON CONFLICT DO UPDATE error
575
+ const uniqueEntitiesMap = new Map();
576
+ for (const entity of processedEntities) {
577
+ if (!entity.slug) {
578
+ entity.slug = `untitled-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
579
+ }
580
+ const uniqueKey = `${entity.language_id}_${entity.slug}`;
581
+ uniqueEntitiesMap.set(uniqueKey, entity);
582
+ }
583
+ const finalEntities = Array.from(uniqueEntitiesMap.values());
584
+
585
+ // Extract _feature_image_id before upsert (not a real DB column)
586
+ const featureImageMap = new Map<string, string>();
587
+ for (const entity of finalEntities) {
588
+ if (entity._feature_image_id) {
589
+ featureImageMap.set(`${entity.language_id}_${entity.slug}`, entity._feature_image_id);
590
+ }
591
+ delete entity._feature_image_id;
592
+ }
593
+
594
+ const { data: upsertedProducts, error: prodErr } = await supabaseClient
595
+ .from('products')
596
+ .upsert(finalEntities, { onConflict: 'language_id, slug' })
597
+ .select('id, slug, language_id');
598
+
599
+ if (prodErr) {
600
+ console.error('Product upsert failed:', prodErr);
601
+ throw prodErr;
602
+ }
603
+
604
+ // Link featured images → product_media junction table
605
+ if (upsertedProducts) {
606
+ const productMediaInserts = [];
607
+ for (const prod of upsertedProducts) {
608
+ const key = `${prod.language_id}_${prod.slug}`;
609
+ const mediaId = featureImageMap.get(key);
610
+ if (mediaId) {
611
+ productMediaInserts.push({
612
+ product_id: prod.id,
613
+ media_id: mediaId,
614
+ sort_order: 0,
615
+ });
616
+ }
617
+ }
618
+ if (productMediaInserts.length > 0) {
619
+ const { error: pmErr } = await supabaseClient.from('product_media').upsert(
620
+ productMediaInserts,
621
+ { onConflict: 'product_id, media_id' }
622
+ );
623
+ if (pmErr) {
624
+ warnings.push(`product_media linking failed: ${pmErr.message}`);
625
+ }
626
+ }
627
+ }
628
+
629
+ result = { error: null };
630
+ }
631
+ }
632
+
633
+ if (result && result.error) throw result.error;
634
+
635
+ return new Response(JSON.stringify({ success: true, processed_count: processedEntities.length, warnings }), {
636
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
637
+ status: 200,
638
+ });
639
+
640
+ } catch (error: any) {
641
+ return new Response(JSON.stringify({ error: error.message }), {
642
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
643
+ status: 500,
644
+ });
645
+ }
646
+ });
@@ -0,0 +1,40 @@
1
+ -- 00000000000000_setup_foundation_and_enums.sql
2
+ -- Consolidated migration preserving original statement order within grouped sections.
3
+
4
+ -- 00000000000000_setup_schema_privileges.sql
5
+ -- Foundation privileges for the public schema.
6
+
7
+ GRANT USAGE ON SCHEMA public TO postgres;
8
+ GRANT USAGE ON SCHEMA public TO anon;
9
+ GRANT USAGE ON SCHEMA public TO authenticated;
10
+ GRANT USAGE ON SCHEMA public TO service_role;
11
+
12
+ -- 00000000000001_setup_auth_and_content_enums.sql
13
+ -- Core enums used by auth and content tables.
14
+
15
+ DO $$
16
+ BEGIN
17
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
18
+ CREATE TYPE public.user_role AS ENUM ('ADMIN', 'WRITER', 'USER');
19
+ END IF;
20
+
21
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'page_status') THEN
22
+ CREATE TYPE public.page_status AS ENUM ('draft', 'published', 'archived');
23
+ END IF;
24
+ END
25
+ $$;
26
+
27
+ -- 00000000000002_setup_navigation_and_revision_enums.sql
28
+ -- Supporting enums for navigation and revision history.
29
+
30
+ DO $$
31
+ BEGIN
32
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'menu_location') THEN
33
+ CREATE TYPE public.menu_location AS ENUM ('HEADER', 'FOOTER', 'SIDEBAR');
34
+ END IF;
35
+
36
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'revision_type') THEN
37
+ CREATE TYPE public.revision_type AS ENUM ('snapshot', 'diff');
38
+ END IF;
39
+ END
40
+ $$;