@bygghammar/cms-platform 0.10.0 → 0.12.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 (64) hide show
  1. package/migrations/20260728113741_f9744b19-078b-46e5-b00f-31c84aaf71ab.sql +111 -0
  2. package/migrations/20260728113757_4156a886-21fb-4a1f-9c12-acb7bb145102.sql +5 -0
  3. package/migrations/20260728113823_6e0237e1-99e3-4e1a-8a68-804b7bcdbbb8.sql +1 -0
  4. package/migrations/20260728115305_506d92b4-129f-4d2b-961f-cf76e592677e.sql +92 -0
  5. package/migrations/20260728115320_717d4447-f433-4a1d-89d2-fc8f028d4ac6.sql +1 -0
  6. package/migrations/20260728115347_c77f5a56-19db-4117-bb60-9ad692b817ab.sql +11 -0
  7. package/migrations/20260728132052_b692dcfe-a944-4a19-afd0-187f7600feb8.sql +63 -0
  8. package/migrations/20260728183318_05b05a47-ba05-4a66-ab8f-69f2688fea84.sql +209 -0
  9. package/migrations/20260728193035_84b68d27-c3f3-4516-a3a9-15fc6f476163.sql +17 -0
  10. package/migrations/20260728193707_8438ee2d-d589-4048-86e2-464fd209af80.sql +37 -0
  11. package/migrations/20260729055312_08862d7e-0279-4d4f-8709-a59b9f068c55.sql +5 -0
  12. package/migrations/20260729055348_418b7e36-4877-4304-831f-290ca73e56f3.sql +133 -0
  13. package/migrations/20260729055358_20b467e2-4efd-42c1-bd34-773ed6b77fc5.sql +3 -0
  14. package/migrations/20260729061039_306859eb-a38c-44ef-af1a-6c02a8fd84db.sql +43 -0
  15. package/migrations/20260730065600_54c25861-3355-4600-a72f-3d67b457ecb1.sql +54 -0
  16. package/migrations/20260730071745_d4904314-15ff-4c58-ac66-85e7371e04b5.sql +113 -0
  17. package/migrations/20260730071914_48163aa3-830e-4dcc-bdd4-c35be282ab87.sql +22 -0
  18. package/migrations/20260730085949_bcfc3270-ea80-4617-86a3-2555ed34a280.sql +24 -0
  19. package/migrations/20260730095256_c977c30e-b285-4041-8804-c0bd72b6d9b3.sql +13 -0
  20. package/migrations/20260730100057_e341b887-eb0e-4a74-850e-0ee5ffb70d7a.sql +38 -0
  21. package/migrations/20260730103703_d5c4e1b7-1e2c-4dd0-be4e-185c159ade69.sql +62 -0
  22. package/migrations/20260730105635_3557b7da-919e-48db-8c92-8221d68d3cf9.sql +29 -0
  23. package/migrations/20260730120308_12ff8cfb-43f9-4070-abde-61531ad3b75f.sql +73 -0
  24. package/migrations/20260730125024_1baf8926-12a2-48f4-8edc-7309941309e9.sql +18 -0
  25. package/migrations/20260730173510_feb974d1-510e-4f52-9e20-5fd0582d8952.sql +107 -0
  26. package/migrations/20260730175409_9f60743f-f5e1-418c-a700-c91fb3704db8.sql +9 -0
  27. package/migrations/20260730181224_1c52ff16-c7a2-4ef1-a0a8-e493c2c9106f.sql +7 -0
  28. package/migrations/20260730183129_933adb75-f985-4d82-ad5d-213b69fd42e5.sql +32 -0
  29. package/migrations/20260730183918_7bc7a827-2100-49f1-bc6e-24509249350b.sql +68 -0
  30. package/migrations/20260730202628_7ceade5e-644c-4bc2-91b8-594133329f31.sql +1 -0
  31. package/migrations/20260730211056_57f42049-00b4-473b-a2cd-65a13570b8d2.sql +16 -0
  32. package/migrations/20260730214340_98af2e55-9031-4c11-9659-c47a37a8d6b7.sql +1 -0
  33. package/migrations/20260731091630_8a6f28ce-5833-4901-ab17-27648cef32ec.sql +149 -0
  34. package/migrations/20260731094716_4c68a1df-83ed-4ba8-8019-fa2fe2b9681e.sql +65 -0
  35. package/migrations/20260731191655_fef97c3c-1397-47b4-8b56-afb020298e97.sql +165 -0
  36. package/migrations/20260731192212_95772a2f-19a1-4013-b463-2eb161e96737.sql +135 -0
  37. package/migrations/20260731192815_c490653e-1232-4ad9-a150-3c83d2b90f5e.sql +13 -0
  38. package/migrations/20260731193135_7966e740-5e86-4b14-8e1d-5119069666ba.sql +2 -0
  39. package/migrations/20260731193245_599790d0-ca5e-4b5d-a3e7-d69b2a79e1a3.sql +13 -0
  40. package/migrations/20260731212959_a748cb0d-4dc7-45dc-be90-f6bacbfab26c.sql +9 -0
  41. package/migrations/20260801003057_1195f4c1-fb54-42dc-9718-638a25e6a82e.sql +3 -0
  42. package/migrations/20260801003300_fc591960-cb41-41cf-837d-2eef23b29ca9.sql +40 -0
  43. package/migrations/20260802151445_a56b0023-eaf7-41e2-a7fb-d4beddfc587c.sql +3 -0
  44. package/migrations/20260802202621_d661af8a-8513-4b54-b375-f963ae824c8f.sql +225 -0
  45. package/migrations/20260802202702_c30cd837-744c-4641-85f1-edd3a7ebbcb6.sql +16 -0
  46. package/migrations/20260802202720_4418d18d-c851-42dc-9345-78fc330128ae.sql +2 -0
  47. package/migrations/20260803132514_78ba1f53-c85b-4457-96b1-7d49c5c8b494.sql +34 -0
  48. package/migrations/20260803181201_28c8aa09-baa8-431c-8338-90b1af9c8172.sql +22 -0
  49. package/migrations/20260803201518_6e8a6028-b70f-4a7d-8bc9-95c86304e7ae.sql +12 -0
  50. package/migrations/20260803201535_32a9a213-cb92-4035-b8cd-8211ca34af68.sql +12 -0
  51. package/migrations/20260811103505_29df753d-14fa-4a05-8fea-f4207785fba3.sql +35 -0
  52. package/migrations/20260811112455_217c4028-f587-4c98-9e4f-9e6c452ab761.sql +20 -0
  53. package/migrations/20260811114438_6687d67d-bad3-4803-9638-8b7670d2e5d4.sql +86 -0
  54. package/migrations/20260811130318_7cb95e95-9baa-40af-8f3f-de1d4e09cec4.sql +1 -0
  55. package/migrations/20260811220548_80d99ec2-bd4b-475d-a627-9408bb60ce56.sql +1 -0
  56. package/migrations/20260811222519_6b040f56-4de3-49e0-92e4-d9deec5ead6a.sql +3 -0
  57. package/migrations/20260812000000_seed_voyara_content.sql +390 -0
  58. package/migrations/20260818040722_1bdf1b8f-7fb0-4ae8-8ecf-73f1fadff3c2.sql +2 -0
  59. package/migrations/20260818040808_69a5e832-57e8-4aa7-aff8-7fd7777faa7c.sql +15 -0
  60. package/migrations/20260818040839_e6523163-bbf5-4a39-9233-91759059569a.sql +26 -0
  61. package/package.json +9 -4
  62. package/src/backoffice/media/ReviewImageUpload.tsx +104 -0
  63. package/src/backoffice/seo/page-seo-form.ts +216 -0
  64. package/src/platform/contract.ts +92 -0
@@ -0,0 +1,165 @@
1
+ insert into public.pages (key, name, status)
2
+ values ('ayia-napa', 'Ayia Napa', 'published')
3
+ on conflict (key) do nothing;
4
+
5
+ with pg as (
6
+ select id from public.pages where key = 'ayia-napa'
7
+ ), nv as (
8
+ insert into public.page_versions (page_id, version_no, note, blocks)
9
+ select pg.id,
10
+ coalesce((select max(version_no) from public.page_versions where page_id = pg.id), 0) + 1,
11
+ 'Rebuilt from barskola.nu/ayia-napa',
12
+ jsonb_build_array(
13
+ jsonb_build_object(
14
+ 'id','an-hero','type','heroFull','visible',true,'props',jsonb_build_object(
15
+ 'mediaType','image',
16
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/ayia-napa-hero-1920.webp',
17
+ 'imageMobile','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/ayia-napa-hero-1920.webp',
18
+ 'imageAlt', jsonb_build_object('sv','Solnedgång över stranden i Ayia Napa på Cypern'),
19
+ 'eyebrow', jsonb_build_object('sv','Cypern'),
20
+ 'title', jsonb_build_object('sv','Ayia Napa'),
21
+ 'body', jsonb_build_object('sv','Europas vackraste stränder, 27 grader redan i maj och ett nattliv som aldrig sover — mitt i din bartenderutbildning.'),
22
+ 'primaryCta', jsonb_build_object('label', jsonb_build_object('sv','Se kursdatum'), 'href','/datum'),
23
+ 'secondaryCta', jsonb_build_object('label', jsonb_build_object('sv','Om bartenderkursen'), 'href','/bartenderkurs'),
24
+ 'height','tall','overlay','bottom','textAlign','left','verticalAlign','bottom',
25
+ 'textTone','light','contentWidth','normal','titleSize','xl','titleFont','serif',
26
+ 'showScrollCue',true,'anchor','ayia-napa'
27
+ )
28
+ ),
29
+ jsonb_build_object(
30
+ 'id','an-intro','type','prose','visible',true,'props',jsonb_build_object(
31
+ 'eyebrow', jsonb_build_object('sv','Destination'),
32
+ 'title', jsonb_build_object('sv','Allt du kan uppleva i Ayia Napa'),
33
+ 'lead', jsonb_build_object('sv','I Ayia Napa finns många olika aktiviteter att göra. Beachvolleyboll, beerpong, hopp från klippor med mera. Självklart erbjuder Ayia Napa mer än bara fest och party.'),
34
+ 'tone','white','layout','two-col',
35
+ 'items', jsonb_build_array(
36
+ jsonb_build_object('id','an-i1','heading',jsonb_build_object('sv','Mer än bara nattliv'),'body',jsonb_build_object('sv','Utöver alla barer och nattklubbar kan man hitta på mängder med aktiviteter till bra priser. I utkanten av Ayia Napa ligger vattenparken Waterworld, en av Europas absolut största vattenparker. Temat för parken är grekisk mytologi och här finner du bland annat Apollos rutschkana, Aeolos virvelpool och Poseidons vågpool.')),
37
+ jsonb_build_object('id','an-i2','heading',jsonb_build_object('sv','Aktiviteter under kursen'),'body',jsonb_build_object('sv','Man kan även spela padel på Ayia Napa Padel Club som har fyra padelbanor. Därtill finns det Bungy Jump, nöjesfält och diverse uthyrning av mopeder, fyrhjulingar och beach buggys. Nedan listar vi några av de populäraste aktiviteterna som du kan uppleva samtidigt som du går bartenderkursen. Observera att eventen sker mot en kostnad och att vissa event är säsongsbetonade och inte erbjuds under alla kurser.'))
38
+ )
39
+ )
40
+ ),
41
+ jsonb_build_object(
42
+ 'id','an-strander','type','mediaSplit','visible',true,'props',jsonb_build_object(
43
+ 'style','editorial','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','paper','aspect','landscape','rounded',false,
44
+ 'mediaType','image',
45
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/nissi-beach-1-1920.webp',
46
+ 'imageAlt', jsonb_build_object('sv','Nissi Beach i Ayia Napa med turkost vatten och vit sand'),
47
+ 'eyebrow', jsonb_build_object('sv','Stränder'),
48
+ 'title', jsonb_build_object('sv','Europas bästa stränder'),
49
+ 'body', jsonb_build_object('sv','Ayia Napa är ett paradis för den som söker vackra stränder, sol och upplevelser. Med Nissi Beach som har framröstats flera år i rad som en av Europas vackraste stränder och med en medeltemperatur på 27 grader redan i maj månad är det det mest självklara valet av semesterort.')
50
+ )
51
+ ),
52
+ jsonb_build_object(
53
+ 'id','an-nattliv','type','mediaSplit','visible',true,'props',jsonb_build_object(
54
+ 'style','dark','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','ink','aspect','landscape','rounded',false,
55
+ 'mediaType','image',
56
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/ayia-napa-nattliv-1920.webp',
57
+ 'imageAlt', jsonb_build_object('sv','Nattliv i Ayia Napa med fullsatt bargata'),
58
+ 'eyebrow', jsonb_build_object('sv','Nattliv'),
59
+ 'title', jsonb_build_object('sv','Fantastiskt nattliv'),
60
+ 'body', jsonb_build_object('sv','För den som söker festligheter och upplevelser går ingen besviken. Med över 200 barer och nattklubbar i Ayia Napa finns det alltid något att välja på, även för den mest kräsna festprissen. Väl på plats finns det även en rad event så som barrundor, partybåtar och poolpartyn. Dessa sker veckovis och är extremt uppskattat bland både turister och de som jobbar på plats.')
61
+ )
62
+ ),
63
+ jsonb_build_object(
64
+ 'id','an-partybat','type','mediaSplit','visible',true,'props',jsonb_build_object(
65
+ 'style','editorial','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','white','aspect','landscape','rounded',false,
66
+ 'mediaType','image',
67
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/partybat-ayia-napa-1920.webp',
68
+ 'imageAlt', jsonb_build_object('sv','Partybåt utanför Ayia Napa med dansande gäster'),
69
+ 'eyebrow', jsonb_build_object('sv','Event'),
70
+ 'title', jsonb_build_object('sv','Partybåt'),
71
+ 'body', jsonb_build_object('sv','Partybåten är en fantastisk upplevelse ute till havs med sol, bad, tävlingar och massa party. DJ:n spelar sommarens absolut hetaste låtar och dansgolvet kokar. Partybåten som är ute till havs i cirka 3,5 timmar brukar ses som den mest uppskattade aktiviteten i Ayia Napa.')
72
+ )
73
+ ),
74
+ jsonb_build_object(
75
+ 'id','an-poolparty','type','mediaSplit','visible',true,'props',jsonb_build_object(
76
+ 'style','editorial','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','paper','aspect','landscape','rounded',false,
77
+ 'mediaType','image',
78
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/poolparty-ayia-napa-1920.webp',
79
+ 'imageAlt', jsonb_build_object('sv','Poolparty på Pambos Napa Rocks i Ayia Napa'),
80
+ 'eyebrow', jsonb_build_object('sv','Event'),
81
+ 'title', jsonb_build_object('sv','Poolparty'),
82
+ 'body', jsonb_build_object('sv','Poolpartyt håller hus på hotellet Pambos Napa Rocks som är ett av Ayia Napas största hotell. Med ett poolområde som rymmer 2 000 personer är det en given plats att ha eventet på.')
83
+ )
84
+ ),
85
+ jsonb_build_object(
86
+ 'id','an-waterworld','type','mediaSplit','visible',true,'props',jsonb_build_object(
87
+ 'style','editorial','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','white','aspect','landscape','rounded',false,
88
+ 'mediaType','image',
89
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/waterworld-ayia-napa-1920.webp',
90
+ 'imageAlt', jsonb_build_object('sv','Vattenrutschkanor i vattenparken Waterworld på Cypern'),
91
+ 'eyebrow', jsonb_build_object('sv','Aktivitet'),
92
+ 'title', jsonb_build_object('sv','Waterworld'),
93
+ 'body', jsonb_build_object('sv','En dag med sol, bad och vattenattraktioner på Cyperns största vattenland. Parken består av mängder av pooler och olika vattenattraktioner för alla åldrar.')
94
+ )
95
+ ),
96
+ jsonb_build_object(
97
+ 'id','an-cape-greco','type','mediaSplit','visible',true,'props',jsonb_build_object(
98
+ 'style','editorial','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','paper','aspect','landscape','rounded',false,
99
+ 'mediaType','image',
100
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/cape-greco-hoppklippor-1920.webp',
101
+ 'imageAlt', jsonb_build_object('sv','Hoppklippor vid Cape Greco utanför Ayia Napa'),
102
+ 'eyebrow', jsonb_build_object('sv','Aktivitet'),
103
+ 'title', jsonb_build_object('sv','Cape Greco (hoppklippor)'),
104
+ 'body', jsonb_build_object('sv','Cape Greco är ett naturreservat strax utanför Ayia Napa. Området har många sevärdheter att erbjuda som till exempel de drygt tolv meter höga klipporna. Dessa klippor ger möjligheten till att hoppa, se den vackra utsikten över Medelhavet eller klättra ner till de gigantiska grottorna.')
105
+ )
106
+ ),
107
+ jsonb_build_object(
108
+ 'id','an-barrunda','type','mediaSplit','visible',true,'props',jsonb_build_object(
109
+ 'style','dark','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','ink','aspect','landscape','rounded',false,
110
+ 'mediaType','image',
111
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/barrunda-ayia-napa-1920.webp',
112
+ 'imageAlt', jsonb_build_object('sv','Barrunda i Ayia Napa med glada deltagare'),
113
+ 'eyebrow', jsonb_build_object('sv','Event'),
114
+ 'title', jsonb_build_object('sv','Barrunda'),
115
+ 'body', jsonb_build_object('sv','Barrundan är ett väldigt populärt event som erbjuds en gång per vecka och är något som Nordic Invasion arrangerar. På barrundan bjuds specialpriser, spex och sköna aktiviteter på varje bar.')
116
+ )
117
+ ),
118
+ jsonb_build_object(
119
+ 'id','an-snorkling','type','mediaSplit','visible',true,'props',jsonb_build_object(
120
+ 'style','editorial','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','white','aspect','landscape','rounded',false,
121
+ 'mediaType','image',
122
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/snorkling-paddleboard-ayia-napa-1920.webp',
123
+ 'imageAlt', jsonb_build_object('sv','Snorkling och paddleboard i det kristallklara vattnet i Ayia Napa'),
124
+ 'eyebrow', jsonb_build_object('sv','Aktivitet'),
125
+ 'title', jsonb_build_object('sv','Snorkling & paddleboard'),
126
+ 'body', jsonb_build_object('sv','Stränderna i Ayia Napa är otroligt fina. På förfrågan ses vi en eftermiddag på någon av de fina stränderna i Ayia Napa där vi njuter av solen, snorklar i det kristallklara havet och paddlar paddleboard.')
127
+ )
128
+ ),
129
+ jsonb_build_object(
130
+ 'id','an-cta','type','ctaBanner','visible',true,'props',jsonb_build_object(
131
+ 'tone','sea',
132
+ 'eyebrow', jsonb_build_object('sv','Nästa säsong'),
133
+ 'title', jsonb_build_object('sv','Upplev Ayia Napa som bartenderelev'),
134
+ 'body', jsonb_build_object('sv','Fyra veckor med utbildning, praktik och allt det här — boende, jobbgaranti och internationellt diplom ingår.'),
135
+ 'cta', jsonb_build_object('label', jsonb_build_object('sv','Se kursdatum'), 'href','/datum'),
136
+ 'secondaryCta', jsonb_build_object('label', jsonb_build_object('sv','Kontakta oss'), 'href','/kontakt')
137
+ )
138
+ ),
139
+ jsonb_build_object('id','an-contact','type','contactCta','visible',true,'props',jsonb_build_object())
140
+ )
141
+ from pg
142
+ returning id, page_id
143
+ )
144
+ insert into public.page_publications (page_id, version_id, note)
145
+ select nv.page_id, nv.id, 'Initial publish of /ayia-napa' from nv;
146
+
147
+ update public.pages p
148
+ set current_version_id = (
149
+ select v.id from public.page_versions v
150
+ where v.page_id = p.id order by v.version_no desc limit 1
151
+ ), status = 'published'
152
+ where p.key = 'ayia-napa';
153
+
154
+ insert into public.page_seo (page_key, slugs, seo)
155
+ values (
156
+ 'ayia-napa',
157
+ jsonb_build_object('sv','ayia-napa'),
158
+ jsonb_build_object(
159
+ 'title', jsonb_build_object('sv','Ayia Napa · Stränder, nattliv & aktiviteter | SBA'),
160
+ 'description', jsonb_build_object('sv','Upptäck Ayia Napa på Cypern — Nissi Beach, över 200 barer, partybåt, poolparty, Waterworld och Cape Greco. Allt du upplever under din bartenderutbildning.'),
161
+ 'ogType','article',
162
+ 'twitterCard','summary_large_image'
163
+ )
164
+ )
165
+ on conflict (page_key) do nothing;
@@ -0,0 +1,135 @@
1
+ insert into public.pages (key, name, status)
2
+ values ('om-oss', 'Om oss', 'published')
3
+ on conflict (key) do nothing;
4
+
5
+ with pg as (
6
+ select id from public.pages where key = 'om-oss'
7
+ ), nv as (
8
+ insert into public.page_versions (page_id, version_no, note, blocks)
9
+ select pg.id,
10
+ coalesce((select max(version_no) from public.page_versions where page_id = pg.id), 0) + 1,
11
+ 'Rebuilt from barskola.nu/kontakta-oss/om-oss',
12
+ jsonb_build_array(
13
+ jsonb_build_object(
14
+ 'id','om-hero','type','heroFull','visible',true,'props',jsonb_build_object(
15
+ 'mediaType','image',
16
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400866656-studerande-elever-1-1920.webp',
17
+ 'imageMobile','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400866656-studerande-elever-1-1920.webp',
18
+ 'imageAlt', jsonb_build_object('sv','Elever studerar tillsammans på Scandinavian Bar Academy'),
19
+ 'eyebrow', jsonb_build_object('sv','Sedan 2004'),
20
+ 'title', jsonb_build_object('sv','Om oss'),
21
+ 'body', jsonb_build_object('sv','Bartenders i absolut världsklass — utbildade i Medelhavet av lärare som brinner för yrket.'),
22
+ 'primaryCta', jsonb_build_object('label', jsonb_build_object('sv','Kontakta oss'), 'href','/kontakt'),
23
+ 'secondaryCta', jsonb_build_object('label', jsonb_build_object('sv','Se kursdatum'), 'href','/datum'),
24
+ 'height','medium','overlay','bottom','textAlign','left','verticalAlign','bottom',
25
+ 'textTone','light','contentWidth','normal','titleSize','xl','titleFont','serif',
26
+ 'showScrollCue',false,'anchor','om-oss'
27
+ )
28
+ ),
29
+ jsonb_build_object(
30
+ 'id','om-marquee','type','imageMarquee','visible',true,'props',jsonb_build_object(
31
+ 'eyebrow', jsonb_build_object('sv','Vår historia'),
32
+ 'title', jsonb_build_object('sv','Världsklass sedan 2004'),
33
+ 'lead', jsonb_build_object('sv','Scandinavian Bar Academy grundades år 2004 och har sedan dess utbildat bartenders i absolut världsklass. Genom vårt unika koncept och enastående lärare håller utbildningen högsta klass. Resultatet är elever av högsta standard som är ständigt eftertraktade på den växande arbetsmarknaden.'),
34
+ 'items2', jsonb_build_array(
35
+ jsonb_build_object('id','om-t1','heading',jsonb_build_object('sv','Minnen för livet'),'body',jsonb_build_object('sv','Vi håller en professionell och hög nivå på lektionerna men strävar också efter att utbildningen och din vistelse hos oss skall ge dig minnen för livet.')),
36
+ jsonb_build_object('id','om-t2','heading',jsonb_build_object('sv','Upplevelsen i centrum'),'body',jsonb_build_object('sv','För oss har upplevelsen en stor betydelse. Därför har vi ett nära samarbete med Nordic Invasion – Sveriges största arrangör av ungdoms- och studentresor.'))
37
+ ),
38
+ 'textPosition','left','columns',2,'firstDirection','up','speed','slow',
39
+ 'aspect','portrait','gap','md','rounded',false,'tone','white','anchor','historia',
40
+ 'items', jsonb_build_array(
41
+ jsonb_build_object('id','om-m1','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400882677-elev-flair-1-1920.webp','alt',jsonb_build_object('sv','Elev tränar flair bakom baren')),
42
+ jsonb_build_object('id','om-m2','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/cassandra-flair-on-beach-dsc02342-1920.webp','alt',jsonb_build_object('sv','Elev flairar på stranden i Ayia Napa')),
43
+ jsonb_build_object('id','om-m3','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/drink-tasting-students-dsc02156-1920.webp','alt',jsonb_build_object('sv','Elever provsmakar drinkar under lektion')),
44
+ jsonb_build_object('id','om-m4','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/barbook-small-1-dsc00447-1920.webp','alt',jsonb_build_object('sv','Kursbok och barverktyg på bardisken')),
45
+ jsonb_build_object('id','om-m5','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/cocktail-dsc02143-1920.webp','alt',jsonb_build_object('sv','Färdig cocktail garnerad av elev')),
46
+ jsonb_build_object('id','om-m6','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400892469-studerande-elever-utomhus-1920.webp','alt',jsonb_build_object('sv','Elever studerar utomhus i solen')),
47
+ jsonb_build_object('id','om-m7','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400897813-magda-flair-1920.webp','alt',jsonb_build_object('sv','Elev flairar med shaker')),
48
+ jsonb_build_object('id','om-m8','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400894305-elever-vid-poolen-med-bok-1920.webp','alt',jsonb_build_object('sv','Elever pluggar vid poolen'))
49
+ )
50
+ )
51
+ ),
52
+ jsonb_build_object(
53
+ 'id','om-nordic','type','mediaSplit','visible',true,'props',jsonb_build_object(
54
+ 'style','editorial','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','paper','aspect','landscape','rounded',false,
55
+ 'mediaType','image',
56
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/barrunda-ayia-napa-1920.webp',
57
+ 'imageAlt', jsonb_build_object('sv','Elever och resenärer på barrunda med Nordic Invasion'),
58
+ 'eyebrow', jsonb_build_object('sv','Samarbete'),
59
+ 'title', jsonb_build_object('sv','Tillsammans med Nordic Invasion'),
60
+ 'body', jsonb_build_object('sv','Tillsammans med Nordic Invasion anordnar vi fester och aktiviteter som är en exklusiv förmån för deras resenärer och våra elever. Detta för att skapa härliga upplevelser utöver studierna.'),
61
+ 'cta', jsonb_build_object('label', jsonb_build_object('sv','Upplev Ayia Napa'), 'href','/ayia-napa')
62
+ )
63
+ ),
64
+ jsonb_build_object(
65
+ 'id','om-mission','type','prose','visible',true,'props',jsonb_build_object(
66
+ 'eyebrow', jsonb_build_object('sv','Mission och vision'),
67
+ 'title', jsonb_build_object('sv','Världens mest omtyckta barskola'),
68
+ 'lead', jsonb_build_object('sv','Scandinavian Bar Academy ska utvecklas och bli den mest framgångsrika och mest omtyckta barskolan i världen.'),
69
+ 'tone','white','layout','two-col',
70
+ 'items', jsonb_build_array(
71
+ jsonb_build_object('id','om-mv1','heading',jsonb_build_object('sv','Vår mission'),'body',jsonb_build_object('sv','Scandinavian Bar Academy ska distribuera eleverna en förstklassig bartenderutbildning och utveckla individen utifrån varje elevs förutsättningar. Scandinavian Bar Academy ska alltid vara professionellt närvarande under varje kursdag och ge eleven dess största upplevelse.')),
72
+ jsonb_build_object('id','om-mv2','heading',jsonb_build_object('sv','Vår vision'),'body',jsonb_build_object('sv','Genom diversifiering och respekt för sina anställda ska Scandinavian Bar Academy stimulera intern kreativitet inom företaget för att därigenom utveckla sin tjänst och alltid ligga i framkant.'))
73
+ )
74
+ )
75
+ ),
76
+ jsonb_build_object(
77
+ 'id','om-kontor','type','mediaSplit','visible',true,'props',jsonb_build_object(
78
+ 'style','dark','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','ink','aspect','landscape','rounded',false,
79
+ 'mediaType','image',
80
+ 'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400908293-barskola-elever-bakifran-pambos-napa-rocks-1920.webp',
81
+ 'imageAlt', jsonb_build_object('sv','Elever framför Pambos Napa Rocks i Ayia Napa'),
82
+ 'eyebrow', jsonb_build_object('sv','Våra kontor'),
83
+ 'title', jsonb_build_object('sv','Ayia Napa & Stockholm'),
84
+ 'body', jsonb_build_object('sv','Scandinavian Bar Academy har kontor i Ayia Napa (Cypern) och i Stockholm (Sverige).
85
+
86
+ Du får mer än gärna besöka oss för ett personligt möte om så önskas. Kontorstiderna i Stockholm är 8–18 alla vardagar. Observera att tidsbokning krävs.'),
87
+ 'bullets', jsonb_build_array(
88
+ jsonb_build_object('id','om-b1','text',jsonb_build_object('sv','Kontor i Ayia Napa, Cypern')),
89
+ jsonb_build_object('id','om-b2','text',jsonb_build_object('sv','Kontor i Stockholm, Sverige')),
90
+ jsonb_build_object('id','om-b3','text',jsonb_build_object('sv','Öppet 8–18 alla vardagar — tidsbokning krävs'))
91
+ ),
92
+ 'cta', jsonb_build_object('label', jsonb_build_object('sv','Våra kontaktuppgifter'), 'href','/kontakt')
93
+ )
94
+ ),
95
+ jsonb_build_object(
96
+ 'id','om-cta','type','ctaBanner','visible',true,'props',jsonb_build_object(
97
+ 'tone','sea',
98
+ 'eyebrow', jsonb_build_object('sv','Vill du veta mer?'),
99
+ 'title', jsonb_build_object('sv','Träffa oss — i Stockholm eller Ayia Napa'),
100
+ 'body', jsonb_build_object('sv','Vi svarar gärna på dina frågor om utbildningen, boendet och livet på plats.'),
101
+ 'cta', jsonb_build_object('label', jsonb_build_object('sv','Kontakta oss'), 'href','/kontakt'),
102
+ 'secondaryCta', jsonb_build_object('label', jsonb_build_object('sv','Se kursdatum'), 'href','/datum')
103
+ )
104
+ ),
105
+ jsonb_build_object('id','om-contact','type','contactCta','visible',true,'props',jsonb_build_object())
106
+ )
107
+ from pg
108
+ returning id, page_id
109
+ )
110
+ insert into public.page_publications (page_id, version_id, note)
111
+ select nv.page_id, nv.id, 'Initial publish of /om-oss' from nv;
112
+
113
+ update public.pages p
114
+ set current_version_id = (
115
+ select v.id from public.page_versions v
116
+ where v.page_id = p.id order by v.version_no desc limit 1
117
+ ), status = 'published'
118
+ where p.key = 'om-oss';
119
+
120
+ insert into public.page_seo (page_key, slugs, seo)
121
+ values (
122
+ 'om-oss',
123
+ jsonb_build_object('sv','om-oss'),
124
+ jsonb_build_object(
125
+ 'title', jsonb_build_object('sv','Om oss · Scandinavian Bar Academy sedan 2004'),
126
+ 'description', jsonb_build_object('sv','Scandinavian Bar Academy grundades 2004 och utbildar bartenders i världsklass. Läs om vår historia, mission och vision samt våra kontor i Ayia Napa och Stockholm.'),
127
+ 'ogType','article',
128
+ 'twitterCard','summary_large_image'
129
+ )
130
+ )
131
+ on conflict (page_key) do nothing;
132
+
133
+ insert into public.redirects (from_path, to_path, status)
134
+ values ('/kontakta-oss/om-oss', '/om-oss', 301)
135
+ on conflict (from_path) do update set to_path = excluded.to_path, status = excluded.status;
@@ -0,0 +1,13 @@
1
+ update page_versions v set blocks = (
2
+ select jsonb_agg(
3
+ case when b->>'id' = 'om-mission' then
4
+ jsonb_build_object('id','om-mission','type','imageMarquee','props', jsonb_build_object(
5
+ 'eyebrow', b->'props'->'eyebrow','title', b->'props'->'title','lead', b->'props'->'lead',
6
+ 'items2', b->'props'->'items','items', $J$[{"id": "om-mm1", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/student-flair-dsc01358-1920.webp", "alt": {"sv": "Elev tränar flair i baren"}}, {"id": "om-mm2", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/students-study-at-beach-dsc01566-1920.webp", "alt": {"sv": "Elever pluggar på stranden"}}, {"id": "om-mm3", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/toni-and-students-1-dsc00166jpg-1920.webp", "alt": {"sv": "Lärare och elever under lektion"}}, {"id": "om-mm4", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/students-and-cocktails-dsc02105-1920.webp", "alt": {"sv": "Elever med färdiga cocktails"}}, {"id": "om-mm5", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/student-pour-1-dsc00573-1920.webp", "alt": {"sv": "Elev häller upp en drink"}}, {"id": "om-mm6", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/ronja-and-sba-flag-1-dsc00188-1920.webp", "alt": {"sv": "Elev med SBA-flaggan"}}]$J$::jsonb,
7
+ 'tone', coalesce(b->'props'->'tone','"white"'::jsonb),'columns', to_jsonb(2),
8
+ 'aspect','"portrait"'::jsonb,'gap','"md"'::jsonb,'speed','"slow"'::jsonb,
9
+ 'firstDirection','"up"'::jsonb,'rounded','false'::jsonb,'textPosition','"right"'::jsonb,
10
+ 'anchor', coalesce(b->'props'->'anchor','""'::jsonb)))
11
+ else b end order by ord)
12
+ from jsonb_array_elements(v.blocks) with ordinality t(b,ord))
13
+ from pages p where p.key='om-oss' and v.id=p.current_version_id;
@@ -0,0 +1,2 @@
1
+ delete from page_seo where page_key = 'om';
2
+ delete from pages where key = 'om';
@@ -0,0 +1,13 @@
1
+ ALTER TABLE public.pages
2
+ ADD COLUMN IF NOT EXISTS deleted_at timestamptz,
3
+ ADD COLUMN IF NOT EXISTS deleted_by uuid,
4
+ ADD COLUMN IF NOT EXISTS deleted_by_email text;
5
+
6
+ CREATE INDEX IF NOT EXISTS pages_deleted_at_idx ON public.pages (deleted_at);
7
+
8
+ DROP POLICY IF EXISTS "Admins delete pages" ON public.pages;
9
+ CREATE POLICY "Superadmins delete pages"
10
+ ON public.pages
11
+ FOR DELETE
12
+ TO authenticated
13
+ USING (has_role(auth.uid(), 'superadmin'::app_role) AND is_system = false);
@@ -0,0 +1,9 @@
1
+ ALTER TABLE public.forms
2
+ ADD COLUMN IF NOT EXISTS deleted_at timestamptz,
3
+ ADD COLUMN IF NOT EXISTS deleted_by uuid,
4
+ ADD COLUMN IF NOT EXISTS deleted_by_email text;
5
+
6
+ DROP POLICY IF EXISTS "forms admin delete" ON public.forms;
7
+ CREATE POLICY "forms superadmin delete" ON public.forms
8
+ FOR DELETE TO authenticated
9
+ USING (public.has_role(auth.uid(), 'superadmin'));
@@ -0,0 +1,3 @@
1
+ ALTER TABLE public.media_focus
2
+ ADD COLUMN IF NOT EXISTS mobile_x smallint CHECK (mobile_x >= 0 AND mobile_x <= 100),
3
+ ADD COLUMN IF NOT EXISTS mobile_y smallint CHECK (mobile_y >= 0 AND mobile_y <= 100);
@@ -0,0 +1,40 @@
1
+ CREATE TABLE IF NOT EXISTS public.media_meta (
2
+ path text PRIMARY KEY,
3
+ alt jsonb NOT NULL DEFAULT '{}'::jsonb,
4
+ title jsonb NOT NULL DEFAULT '{}'::jsonb,
5
+ description jsonb NOT NULL DEFAULT '{}'::jsonb,
6
+ credit text NOT NULL DEFAULT '',
7
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
8
+ updated_at timestamp with time zone NOT NULL DEFAULT now(),
9
+ updated_by uuid
10
+ );
11
+
12
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.media_meta TO authenticated;
13
+ GRANT ALL ON public.media_meta TO service_role;
14
+
15
+ ALTER TABLE public.media_meta ENABLE ROW LEVEL SECURITY;
16
+
17
+ CREATE POLICY "Staff read media_meta" ON public.media_meta
18
+ FOR SELECT TO authenticated USING (public.is_staff(auth.uid()));
19
+ CREATE POLICY "Staff insert media_meta" ON public.media_meta
20
+ FOR INSERT TO authenticated WITH CHECK (public.is_staff(auth.uid()));
21
+ CREATE POLICY "Staff update media_meta" ON public.media_meta
22
+ FOR UPDATE TO authenticated USING (public.is_staff(auth.uid())) WITH CHECK (public.is_staff(auth.uid()));
23
+ CREATE POLICY "Staff delete media_meta" ON public.media_meta
24
+ FOR DELETE TO authenticated USING (public.is_staff(auth.uid()));
25
+
26
+ CREATE OR REPLACE FUNCTION public.touch_media_meta()
27
+ RETURNS trigger
28
+ LANGUAGE plpgsql
29
+ SET search_path = public
30
+ AS $$
31
+ BEGIN
32
+ NEW.updated_at = now();
33
+ NEW.updated_by = auth.uid();
34
+ RETURN NEW;
35
+ END;
36
+ $$;
37
+
38
+ CREATE TRIGGER touch_media_meta_before_update
39
+ BEFORE UPDATE ON public.media_meta
40
+ FOR EACH ROW EXECUTE FUNCTION public.touch_media_meta();
@@ -0,0 +1,3 @@
1
+ UPDATE public.global_sections SET current_version_id = NULL WHERE key LIKE 'tmp-verify%';
2
+ DELETE FROM public.global_section_versions WHERE section_id IN (SELECT id FROM public.global_sections WHERE key LIKE 'tmp-verify%');
3
+ DELETE FROM public.global_sections WHERE key LIKE 'tmp-verify%';
@@ -0,0 +1,225 @@
1
+ -- 1. Portability layer -------------------------------------------------
2
+ CREATE SCHEMA IF NOT EXISTS app;
3
+ GRANT USAGE ON SCHEMA app TO anon, authenticated, service_role;
4
+
5
+ -- Single point of indirection. On a vanilla Postgres install this function is
6
+ -- replaced by db/bootstrap.sql to read a session setting instead.
7
+ CREATE OR REPLACE FUNCTION app.current_user_id()
8
+ RETURNS uuid
9
+ LANGUAGE sql
10
+ STABLE
11
+ SECURITY DEFINER
12
+ SET search_path = auth, public, pg_temp
13
+ AS $$
14
+ SELECT auth.uid()
15
+ $$;
16
+
17
+ CREATE OR REPLACE FUNCTION app.has_role(_user_id uuid, _role public.app_role)
18
+ RETURNS boolean
19
+ LANGUAGE sql
20
+ STABLE
21
+ SECURITY DEFINER
22
+ SET search_path = public
23
+ AS $$
24
+ SELECT EXISTS (
25
+ SELECT 1 FROM public.user_roles
26
+ WHERE user_id = _user_id
27
+ AND (role = _role OR (role = 'superadmin'::public.app_role AND _role <> 'superadmin'::public.app_role))
28
+ )
29
+ $$;
30
+
31
+ CREATE OR REPLACE FUNCTION app.is_staff(_user_id uuid)
32
+ RETURNS boolean
33
+ LANGUAGE sql
34
+ STABLE
35
+ SECURITY DEFINER
36
+ SET search_path = public
37
+ AS $$
38
+ SELECT EXISTS (
39
+ SELECT 1 FROM public.user_roles
40
+ WHERE user_id = _user_id AND role IN ('admin','superadmin')
41
+ )
42
+ $$;
43
+
44
+ GRANT EXECUTE ON FUNCTION app.current_user_id() TO anon, authenticated, service_role;
45
+ GRANT EXECUTE ON FUNCTION app.has_role(uuid, public.app_role) TO anon, authenticated, service_role;
46
+ GRANT EXECUTE ON FUNCTION app.is_staff(uuid) TO anon, authenticated, service_role;
47
+
48
+ -- Existing public helpers become thin wrappers so application code is unchanged.
49
+ CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role public.app_role)
50
+ RETURNS boolean
51
+ LANGUAGE sql
52
+ STABLE
53
+ SECURITY DEFINER
54
+ SET search_path = public
55
+ AS $$
56
+ SELECT app.has_role(_user_id, _role)
57
+ $$;
58
+
59
+ CREATE OR REPLACE FUNCTION public.is_staff(_user_id uuid)
60
+ RETURNS boolean
61
+ LANGUAGE sql
62
+ STABLE
63
+ SECURITY DEFINER
64
+ SET search_path = public
65
+ AS $$
66
+ SELECT app.is_staff(_user_id)
67
+ $$;
68
+
69
+ -- 2. Own the user table -------------------------------------------------
70
+ CREATE TABLE IF NOT EXISTS public.app_users (
71
+ id uuid PRIMARY KEY,
72
+ email text,
73
+ display_name text,
74
+ created_at timestamptz NOT NULL DEFAULT now(),
75
+ updated_at timestamptz NOT NULL DEFAULT now(),
76
+ last_seen_at timestamptz
77
+ );
78
+
79
+ GRANT SELECT ON public.app_users TO authenticated;
80
+ GRANT ALL ON public.app_users TO service_role;
81
+ ALTER TABLE public.app_users ENABLE ROW LEVEL SECURITY;
82
+
83
+ DROP POLICY IF EXISTS "Users can view themselves" ON public.app_users;
84
+ CREATE POLICY "Users can view themselves" ON public.app_users
85
+ FOR SELECT TO authenticated USING (id = app.current_user_id());
86
+
87
+ DROP POLICY IF EXISTS "Staff view app_users" ON public.app_users;
88
+ CREATE POLICY "Staff view app_users" ON public.app_users
89
+ FOR SELECT TO authenticated USING (public.is_staff(app.current_user_id()));
90
+
91
+ CREATE OR REPLACE FUNCTION public.touch_app_users()
92
+ RETURNS trigger LANGUAGE plpgsql SET search_path = public AS $$
93
+ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$;
94
+
95
+ DROP TRIGGER IF EXISTS touch_app_users ON public.app_users;
96
+ CREATE TRIGGER touch_app_users BEFORE UPDATE ON public.app_users
97
+ FOR EACH ROW EXECUTE FUNCTION public.touch_app_users();
98
+
99
+ -- Backfill every existing account.
100
+ INSERT INTO public.app_users (id, email, display_name, created_at)
101
+ SELECT u.id,
102
+ u.email,
103
+ COALESCE(u.raw_user_meta_data->>'full_name', u.raw_user_meta_data->>'name'),
104
+ u.created_at
105
+ FROM auth.users u
106
+ ON CONFLICT (id) DO NOTHING;
107
+
108
+ -- Keeps app_users complete whenever a role is granted, so the foreign key
109
+ -- below can never block a legitimate insert.
110
+ CREATE OR REPLACE FUNCTION public.ensure_app_user()
111
+ RETURNS trigger
112
+ LANGUAGE plpgsql
113
+ SECURITY DEFINER
114
+ SET search_path = public, auth, pg_temp
115
+ AS $$
116
+ BEGIN
117
+ INSERT INTO public.app_users (id, email, display_name, created_at)
118
+ SELECT u.id,
119
+ u.email,
120
+ COALESCE(u.raw_user_meta_data->>'full_name', u.raw_user_meta_data->>'name'),
121
+ u.created_at
122
+ FROM auth.users u
123
+ WHERE u.id = NEW.user_id
124
+ ON CONFLICT (id) DO NOTHING;
125
+
126
+ INSERT INTO public.app_users (id) VALUES (NEW.user_id)
127
+ ON CONFLICT (id) DO NOTHING;
128
+
129
+ RETURN NEW;
130
+ END;
131
+ $$;
132
+
133
+ DROP TRIGGER IF EXISTS ensure_app_user_before_role ON public.user_roles;
134
+ CREATE TRIGGER ensure_app_user_before_role
135
+ BEFORE INSERT ON public.user_roles
136
+ FOR EACH ROW EXECUTE FUNCTION public.ensure_app_user();
137
+
138
+ -- Also record the account when the initial admin trigger fires.
139
+ CREATE OR REPLACE FUNCTION public.grant_initial_admin()
140
+ RETURNS trigger
141
+ LANGUAGE plpgsql
142
+ SECURITY DEFINER
143
+ SET search_path = public
144
+ AS $$
145
+ BEGIN
146
+ IF NEW.email_confirmed_at IS NOT NULL THEN
147
+ INSERT INTO public.app_users (id, email, display_name, created_at)
148
+ VALUES (NEW.id, NEW.email,
149
+ COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name'),
150
+ NEW.created_at)
151
+ ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email;
152
+ END IF;
153
+
154
+ IF NEW.email_confirmed_at IS NOT NULL
155
+ AND lower(NEW.email) = 'byggis@gmail.com' THEN
156
+ INSERT INTO public.user_roles (user_id, role) VALUES (NEW.id, 'admin')
157
+ ON CONFLICT (user_id, role) DO NOTHING;
158
+ INSERT INTO public.user_roles (user_id, role) VALUES (NEW.id, 'superadmin')
159
+ ON CONFLICT (user_id, role) DO NOTHING;
160
+ END IF;
161
+ RETURN NEW;
162
+ END;
163
+ $$;
164
+
165
+ -- Re-point the two foreign keys away from the hosted auth table.
166
+ ALTER TABLE public.user_roles DROP CONSTRAINT IF EXISTS user_roles_user_id_fkey;
167
+ ALTER TABLE public.user_roles
168
+ ADD CONSTRAINT user_roles_user_id_fkey
169
+ FOREIGN KEY (user_id) REFERENCES public.app_users(id) ON DELETE CASCADE;
170
+
171
+ ALTER TABLE public.content_sections DROP CONSTRAINT IF EXISTS content_sections_updated_by_fkey;
172
+ ALTER TABLE public.content_sections
173
+ ADD CONSTRAINT content_sections_updated_by_fkey
174
+ FOREIGN KEY (updated_by) REFERENCES public.app_users(id) ON DELETE SET NULL;
175
+
176
+ -- 3. Route every policy through the portability layer -------------------
177
+ DO $do$
178
+ DECLARE
179
+ p record;
180
+ stmt text;
181
+ BEGIN
182
+ FOR p IN
183
+ SELECT tablename, policyname, permissive, cmd, roles, qual, with_check
184
+ FROM pg_policies
185
+ WHERE schemaname = 'public'
186
+ AND (qual LIKE '%auth.uid()%' OR with_check LIKE '%auth.uid()%')
187
+ LOOP
188
+ stmt := format(
189
+ 'DROP POLICY %I ON public.%I',
190
+ p.policyname, p.tablename
191
+ );
192
+ EXECUTE stmt;
193
+
194
+ stmt := format(
195
+ 'CREATE POLICY %I ON public.%I AS %s FOR %s TO %s',
196
+ p.policyname, p.tablename, p.permissive, p.cmd,
197
+ array_to_string(p.roles, ', ')
198
+ );
199
+ IF p.qual IS NOT NULL THEN
200
+ stmt := stmt || ' USING (' || replace(p.qual, 'auth.uid()', 'app.current_user_id()') || ')';
201
+ END IF;
202
+ IF p.with_check IS NOT NULL THEN
203
+ stmt := stmt || ' WITH CHECK (' || replace(p.with_check, 'auth.uid()', 'app.current_user_id()') || ')';
204
+ END IF;
205
+ EXECUTE stmt;
206
+ END LOOP;
207
+ END
208
+ $do$;
209
+
210
+ -- 4. Triggers use the portability layer too ------------------------------
211
+ DO $do$
212
+ DECLARE
213
+ f record;
214
+ BEGIN
215
+ FOR f IN
216
+ SELECT p.oid, p.proname, pg_get_functiondef(p.oid) AS def
217
+ FROM pg_proc p
218
+ WHERE p.pronamespace = 'public'::regnamespace
219
+ AND p.proname LIKE 'touch\_%'
220
+ AND pg_get_functiondef(p.oid) LIKE '%auth.uid()%'
221
+ LOOP
222
+ EXECUTE replace(f.def, 'auth.uid()', 'app.current_user_id()');
223
+ END LOOP;
224
+ END
225
+ $do$;