@nextblock-cms/db 0.17.5 → 0.17.7
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.
- package/package.json +1 -1
- package/supabase/migrations/02010_shop_hero_redesign.sql +72 -0
- package/supabase/migrations/02011_drop_backdrop_blur_on_pages.sql +26 -0
- package/supabase/migrations/02012_drop_variant_backdrop_blur.sql +17 -0
- package/supabase/migrations/02013_advisor_rls_and_trigger_grants.sql +102 -0
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
-- 02010: shop hero redesign -- shorter copy, a product image, and a compact benefits
|
|
2
|
+
-- strip below the product grid (EN 'shop' and FR 'boutique').
|
|
3
|
+
--
|
|
4
|
+
-- The previous hero was a full-width primary-coloured block holding an h1, an intro
|
|
5
|
+
-- line, a second heading, two long paragraphs and a four-item bullet list: ~1,000 px
|
|
6
|
+
-- of text and no image, written to satisfy the SEO grader rather than a visitor. It
|
|
7
|
+
-- now mirrors the home hero: dark gradient, two columns, one h1, one sentence, two
|
|
8
|
+
-- CTAs, and the NextBlock Commerce storefront image (bundled `/images/commerce-wide.webp`,
|
|
9
|
+
-- which the rich-text renderer serves through next/image; as the first hero image it
|
|
10
|
+
-- is preloaded as the page's LCP element). The four purchase benefits move into a
|
|
11
|
+
-- compact card strip after the product grid so the page keeps its supporting copy
|
|
12
|
+
-- without it living in the hero.
|
|
13
|
+
--
|
|
14
|
+
-- Both languages ship together (bilingual content rule). Scoped by parent page +
|
|
15
|
+
-- position + a signature of the OLD copy, never by blocks.id: each UPDATE runs once
|
|
16
|
+
-- and a re-run matches nothing. The benefits strip is inserted once per page, guarded
|
|
17
|
+
-- by its `nb-store-benefits` sentinel. Identity sequences are re-synced first because
|
|
18
|
+
-- the seed inserts explicit ids.
|
|
19
|
+
|
|
20
|
+
DO $$
|
|
21
|
+
DECLARE
|
|
22
|
+
v_en bigint;
|
|
23
|
+
v_fr bigint;
|
|
24
|
+
v_shop bigint;
|
|
25
|
+
v_boutique bigint;
|
|
26
|
+
BEGIN
|
|
27
|
+
SELECT id INTO v_en FROM public.languages WHERE code = 'en' LIMIT 1;
|
|
28
|
+
SELECT id INTO v_fr FROM public.languages WHERE code = 'fr' LIMIT 1;
|
|
29
|
+
IF v_en IS NULL THEN
|
|
30
|
+
RAISE NOTICE '02010: no English language row; nothing to do.';
|
|
31
|
+
RETURN;
|
|
32
|
+
END IF;
|
|
33
|
+
|
|
34
|
+
SELECT id INTO v_shop FROM public.pages WHERE slug = 'shop' AND language_id = v_en LIMIT 1;
|
|
35
|
+
IF v_fr IS NOT NULL THEN
|
|
36
|
+
SELECT id INTO v_boutique FROM public.pages WHERE slug = 'boutique' AND language_id = v_fr LIMIT 1;
|
|
37
|
+
END IF;
|
|
38
|
+
|
|
39
|
+
PERFORM setval('public.blocks_id_seq', COALESCE((SELECT MAX(id) FROM public.blocks), 0) + 1, false);
|
|
40
|
+
|
|
41
|
+
-- ---------------------------------------------------------------- EN: /shop
|
|
42
|
+
IF v_shop IS NOT NULL THEN
|
|
43
|
+
UPDATE public.blocks
|
|
44
|
+
SET content = $nb${"is_hero":true,"padding":{"top":"lg","bottom":"lg"},"background":{"type":"gradient","gradient":{"type":"linear","direction":"135deg","stops":[{"color":"#020817","position":0},{"color":"#0f172a","position":50},{"color":"#1e293b","position":100}]}},"column_gap":"xl","container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class='text-xs uppercase tracking-[0.3em] text-cyan-300 font-semibold mb-3'>Official store</p><h1 class='text-4xl md:text-5xl font-extrabold text-white leading-tight mb-4'>NextBlock™ Store</h1><p class='text-lg text-slate-300 leading-relaxed mb-2'>Official licenses for NextBlock™ Commerce and Cortex AI. Instant access, full source code and secure checkout with Stripe and Freemius.</p>"}},{"block_type":"button","content":{"text":"Get NextBlock™ Commerce","url":"/product/nextblock-commerce-pro-commerce-license","variant":"default","size":"lg","position":"left"}},{"block_type":"button","content":{"text":"Start the free Cortex AI trial","url":"/product/nextblock-cortex-ai-cortex-ai-license","variant":"outline","size":"lg","position":"left"}}],[{"block_type":"text","content":{"html_content":"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 p-3 shadow-2xl'><img src='/images/commerce-wide.webp' alt='NextBlock™ Commerce storefront with catalog, cart and checkout' class='w-full h-auto rounded-2xl' /></div>"}}]]}$nb$::jsonb,
|
|
45
|
+
updated_at = now()
|
|
46
|
+
WHERE page_id = v_shop
|
|
47
|
+
AND "order" = 0
|
|
48
|
+
AND block_type = 'section'
|
|
49
|
+
AND content::text LIKE '%Discover our selection of official commercial add-ons%';
|
|
50
|
+
|
|
51
|
+
IF NOT EXISTS (SELECT 1 FROM public.blocks WHERE page_id = v_shop AND content::text LIKE '%nb-store-benefits%') THEN
|
|
52
|
+
INSERT INTO public.blocks (page_id, language_id, block_type, content, "order")
|
|
53
|
+
VALUES (v_shop, v_en, 'section', $nb${"padding":{"top":"lg","bottom":"xl"},"background":{"type":"none"},"column_gap":"md","container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"vertical_alignment":"start","column_blocks":[[{"block_type":"text","content":{"html_content":"<div class='nb-store-benefits'><h2 class='text-2xl md:text-3xl font-bold text-foreground text-center mb-8'>Why buy from the official store</h2><div class='grid gap-6 md:grid-cols-2 lg:grid-cols-4'><div class='rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900'><p class='text-2xl mb-3'>🧩</p><h3 class='text-base font-bold text-foreground mb-2'>Full source code</h3><p class='text-sm text-slate-600 dark:text-slate-400'>Inspect, customize and adapt every module to your exact requirements.</p></div><div class='rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900'><p class='text-2xl mb-3'>♾️</p><h3 class='text-base font-bold text-foreground mb-2'>Perpetual use</h3><p class='text-sm text-slate-600 dark:text-slate-400'>Use the software on your project with full peace of mind.</p></div><div class='rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900'><p class='text-2xl mb-3'>🔄</p><h3 class='text-base font-bold text-foreground mb-2'>Automated updates</h3><p class='text-sm text-slate-600 dark:text-slate-400'>Smooth updates that follow each release of NextBlock and Next.js.</p></div><div class='rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900'><p class='text-2xl mb-3'>🔒</p><h3 class='text-base font-bold text-foreground mb-2'>Secure checkout</h3><p class='text-sm text-slate-600 dark:text-slate-400'>Payments processed safely by Stripe and Freemius, invoice included.</p></div></div></div>"}}]]}$nb$::jsonb, 2);
|
|
54
|
+
END IF;
|
|
55
|
+
END IF;
|
|
56
|
+
|
|
57
|
+
-- ---------------------------------------------------------------- FR: /boutique
|
|
58
|
+
IF v_boutique IS NOT NULL THEN
|
|
59
|
+
UPDATE public.blocks
|
|
60
|
+
SET content = $nb${"is_hero":true,"padding":{"top":"lg","bottom":"lg"},"background":{"type":"gradient","gradient":{"type":"linear","direction":"135deg","stops":[{"color":"#020817","position":0},{"color":"#0f172a","position":50},{"color":"#1e293b","position":100}]}},"column_gap":"xl","container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":2},"vertical_alignment":"center","column_blocks":[[{"block_type":"text","content":{"html_content":"<p class='text-xs uppercase tracking-[0.3em] text-cyan-300 font-semibold mb-3'>Boutique officielle</p><h1 class='text-4xl md:text-5xl font-extrabold text-white leading-tight mb-4'>Boutique NextBlock™</h1><p class='text-lg text-slate-300 leading-relaxed mb-2'>Licences officielles pour NextBlock™ Commerce et Cortex AI. Accès immédiat, code source complet et paiement sécurisé avec Stripe et Freemius.</p>"}},{"block_type":"button","content":{"text":"Obtenir NextBlock™ Commerce","url":"/product/nextblock-commerce-pro-commerce-license-fr","variant":"default","size":"lg","position":"left"}},{"block_type":"button","content":{"text":"Commencer l'essai gratuit de Cortex AI","url":"/product/nextblock-cortex-ai-cortex-ai-license-fr","variant":"outline","size":"lg","position":"left"}}],[{"block_type":"text","content":{"html_content":"<div class='rounded-3xl overflow-hidden border border-white/10 bg-white/5 p-3 shadow-2xl'><img src='/images/commerce-wide.webp' alt='Vitrine NextBlock™ Commerce avec catalogue, panier et paiement' class='w-full h-auto rounded-2xl' /></div>"}}]]}$nb$::jsonb,
|
|
61
|
+
updated_at = now()
|
|
62
|
+
WHERE page_id = v_boutique
|
|
63
|
+
AND "order" = 0
|
|
64
|
+
AND block_type = 'section'
|
|
65
|
+
AND content::text LIKE '%nos extensions officielles et nos outils%';
|
|
66
|
+
|
|
67
|
+
IF NOT EXISTS (SELECT 1 FROM public.blocks WHERE page_id = v_boutique AND content::text LIKE '%nb-store-benefits%') THEN
|
|
68
|
+
INSERT INTO public.blocks (page_id, language_id, block_type, content, "order")
|
|
69
|
+
VALUES (v_boutique, v_fr, 'section', $nb${"padding":{"top":"lg","bottom":"xl"},"background":{"type":"none"},"column_gap":"md","container_type":"container","responsive_columns":{"mobile":1,"tablet":1,"desktop":1},"vertical_alignment":"start","column_blocks":[[{"block_type":"text","content":{"html_content":"<div class='nb-store-benefits'><h2 class='text-2xl md:text-3xl font-bold text-foreground text-center mb-8'>Pourquoi acheter sur la boutique officielle</h2><div class='grid gap-6 md:grid-cols-2 lg:grid-cols-4'><div class='rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900'><p class='text-2xl mb-3'>🧩</p><h3 class='text-base font-bold text-foreground mb-2'>Code source complet</h3><p class='text-sm text-slate-600 dark:text-slate-400'>Lisez, adaptez et faites évoluer chaque module selon vos besoins.</p></div><div class='rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900'><p class='text-2xl mb-3'>♾️</p><h3 class='text-base font-bold text-foreground mb-2'>Licence perpétuelle</h3><p class='text-sm text-slate-600 dark:text-slate-400'>Utilisez le logiciel sur votre projet en toute sérénité.</p></div><div class='rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900'><p class='text-2xl mb-3'>🔄</p><h3 class='text-base font-bold text-foreground mb-2'>Mises à jour suivies</h3><p class='text-sm text-slate-600 dark:text-slate-400'>Des mises à jour fluides qui suivent chaque version de NextBlock et de Next.js.</p></div><div class='rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900'><p class='text-2xl mb-3'>🔒</p><h3 class='text-base font-bold text-foreground mb-2'>Paiement sécurisé</h3><p class='text-sm text-slate-600 dark:text-slate-400'>Paiements traités en toute sécurité par Stripe et Freemius, facture incluse.</p></div></div></div>"}}]]}$nb$::jsonb, 2);
|
|
70
|
+
END IF;
|
|
71
|
+
END IF;
|
|
72
|
+
END $$;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- 02011: remove `backdrop-blur-*` from page blocks (home EN/FR cards, articles heroes).
|
|
2
|
+
--
|
|
3
|
+
-- Every card sitting on the dark gradient heroes used `backdrop-filter: blur()` via
|
|
4
|
+
-- Tailwind's backdrop-blur utilities (`backdrop-blur-xl` on the home hero card and the
|
|
5
|
+
-- live-demo promo, `backdrop-blur` on the three MCP-step cards, `backdrop-blur-sm` on
|
|
6
|
+
-- the commerce card and the articles heroes). A backdrop blur forces the compositor to
|
|
7
|
+
-- read back and blur everything behind the element on every frame; in Lighthouse's
|
|
8
|
+
-- trace of the home page the GPU process spent ~390 ms drawing the very first frame,
|
|
9
|
+
-- which is the gap between the hero being laid out and the first contentful paint.
|
|
10
|
+
-- Behind these cards there is only a smooth gradient or a solid colour, so the blur has
|
|
11
|
+
-- no visible effect: `bg-white/5` alone gives the same translucent card.
|
|
12
|
+
--
|
|
13
|
+
-- Scoped to page blocks that carry the utility (posts and products never did), both
|
|
14
|
+
-- languages at once; `regexp_replace` strips the class token wherever it appears in a
|
|
15
|
+
-- class list. Idempotent: once stripped, the row no longer matches. page_revisions get
|
|
16
|
+
-- the same rewrite so restoring a revision cannot bring the blur back.
|
|
17
|
+
|
|
18
|
+
UPDATE public.blocks
|
|
19
|
+
SET content = regexp_replace(content::text, '\sbackdrop-blur(-[a-z0-9]+)?', '', 'g')::jsonb,
|
|
20
|
+
updated_at = now()
|
|
21
|
+
WHERE page_id IS NOT NULL
|
|
22
|
+
AND content::text ~ 'backdrop-blur';
|
|
23
|
+
|
|
24
|
+
UPDATE public.page_revisions
|
|
25
|
+
SET content = regexp_replace(content::text, '\sbackdrop-blur(-[a-z0-9]+)?', '', 'g')::jsonb
|
|
26
|
+
WHERE content::text ~ 'backdrop-blur';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- 02012: follow-up to 02011 -- also strip variant-prefixed backdrop blurs.
|
|
2
|
+
--
|
|
3
|
+
-- 02011 removed bare `backdrop-blur*` tokens, but the live-demo promo card on the EN
|
|
4
|
+
-- and FR home pages carries `dark:backdrop-blur-xl`, whose token starts with a variant
|
|
5
|
+
-- prefix and so did not match `\sbackdrop-blur`. Same reasoning as 02011: the card sits
|
|
6
|
+
-- on a solid slate background in dark mode, the blur is invisible and costs a GPU
|
|
7
|
+
-- read-back per frame. Idempotent and scoped exactly like 02011.
|
|
8
|
+
|
|
9
|
+
UPDATE public.blocks
|
|
10
|
+
SET content = regexp_replace(content::text, '\s([a-z]+:)?backdrop-blur(-[a-z0-9]+)?', '', 'g')::jsonb,
|
|
11
|
+
updated_at = now()
|
|
12
|
+
WHERE page_id IS NOT NULL
|
|
13
|
+
AND content::text ~ 'backdrop-blur';
|
|
14
|
+
|
|
15
|
+
UPDATE public.page_revisions
|
|
16
|
+
SET content = regexp_replace(content::text, '\s([a-z]+:)?backdrop-blur(-[a-z0-9]+)?', '', 'g')::jsonb
|
|
17
|
+
WHERE content::text ~ 'backdrop-blur';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
-- 02013: clear the Supabase Advisor warnings on RLS policies and trigger-function grants.
|
|
2
|
+
--
|
|
3
|
+
-- Performance advisor
|
|
4
|
+
-- * auth_rls_initplan: cms_interactions_insert_policy called auth.uid() per candidate row.
|
|
5
|
+
-- Wrapped as (select auth.uid()) it becomes an InitPlan evaluated once per statement.
|
|
6
|
+
-- * multiple_permissive_policies: cms_redirects and site_scripts each had two SELECT
|
|
7
|
+
-- policies for `authenticated` ("Admins read all …" and "Public read active …"), and
|
|
8
|
+
-- form_endpoints had an admin FOR ALL policy overlapping the editor SELECT policy. Every
|
|
9
|
+
-- permissive policy that matches a role + command is evaluated for every row, so the
|
|
10
|
+
-- policies are re-cut so exactly one applies per role + command. Who can read or write
|
|
11
|
+
-- what is unchanged:
|
|
12
|
+
-- - anon reads active redirects / scripts;
|
|
13
|
+
-- - authenticated reads active rows, ADMIN reads all;
|
|
14
|
+
-- - form_endpoints: ADMIN + WRITER read, ADMIN writes (now three command-scoped
|
|
15
|
+
-- policies instead of FOR ALL), service_role unchanged.
|
|
16
|
+
--
|
|
17
|
+
-- Security advisor
|
|
18
|
+
-- * anon_/authenticated_security_definer_function_executable: handle_new_user() and
|
|
19
|
+
-- update_product_ratings() are SECURITY DEFINER trigger functions. Postgres checks
|
|
20
|
+
-- EXECUTE on a trigger function only at CREATE TRIGGER time, never when the trigger
|
|
21
|
+
-- fires (and PostgREST cannot call a `RETURNS trigger` function via /rpc anyway), so
|
|
22
|
+
-- anon and authenticated never needed the privilege. 02003 already revokes it on
|
|
23
|
+
-- handle_new_user() for fresh installs, but production recorded the baseline as applied
|
|
24
|
+
-- without running it (docs/04 → "How a squash crosses live databases"), so the
|
|
25
|
+
-- generation-1 grants were still live there. Idempotent: safe on a fresh install too.
|
|
26
|
+
--
|
|
27
|
+
-- The two Auth-dashboard warnings (leaked password protection, MFA options) are project
|
|
28
|
+
-- settings, not schema, and are not addressed here.
|
|
29
|
+
|
|
30
|
+
-- ---------------------------------------------------------------------------
|
|
31
|
+
-- cms_interactions: InitPlan-friendly auth.uid()
|
|
32
|
+
-- ---------------------------------------------------------------------------
|
|
33
|
+
DROP POLICY IF EXISTS cms_interactions_insert_policy ON public.cms_interactions;
|
|
34
|
+
CREATE POLICY cms_interactions_insert_policy ON public.cms_interactions
|
|
35
|
+
FOR INSERT TO authenticated
|
|
36
|
+
WITH CHECK (
|
|
37
|
+
((SELECT auth.uid()) = user_id)
|
|
38
|
+
AND (
|
|
39
|
+
status = 'pending'::public.approval_status
|
|
40
|
+
OR (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role])
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
-- ---------------------------------------------------------------------------
|
|
45
|
+
-- cms_redirects: one SELECT policy per role
|
|
46
|
+
-- ---------------------------------------------------------------------------
|
|
47
|
+
DROP POLICY IF EXISTS "Admins read all redirects" ON public.cms_redirects;
|
|
48
|
+
DROP POLICY IF EXISTS "Public read active redirects" ON public.cms_redirects;
|
|
49
|
+
DROP POLICY IF EXISTS "Authenticated read redirects" ON public.cms_redirects;
|
|
50
|
+
|
|
51
|
+
CREATE POLICY "Public read active redirects" ON public.cms_redirects
|
|
52
|
+
FOR SELECT TO anon
|
|
53
|
+
USING (is_active);
|
|
54
|
+
|
|
55
|
+
CREATE POLICY "Authenticated read redirects" ON public.cms_redirects
|
|
56
|
+
FOR SELECT TO authenticated
|
|
57
|
+
USING (is_active OR (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);
|
|
58
|
+
|
|
59
|
+
-- ---------------------------------------------------------------------------
|
|
60
|
+
-- site_scripts: one SELECT policy per role
|
|
61
|
+
-- ---------------------------------------------------------------------------
|
|
62
|
+
DROP POLICY IF EXISTS "Admins read all site scripts" ON public.site_scripts;
|
|
63
|
+
DROP POLICY IF EXISTS "Public read active site scripts" ON public.site_scripts;
|
|
64
|
+
DROP POLICY IF EXISTS "Authenticated read site scripts" ON public.site_scripts;
|
|
65
|
+
|
|
66
|
+
CREATE POLICY "Public read active site scripts" ON public.site_scripts
|
|
67
|
+
FOR SELECT TO anon
|
|
68
|
+
USING (is_active);
|
|
69
|
+
|
|
70
|
+
CREATE POLICY "Authenticated read site scripts" ON public.site_scripts
|
|
71
|
+
FOR SELECT TO authenticated
|
|
72
|
+
USING (is_active OR (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);
|
|
73
|
+
|
|
74
|
+
-- ---------------------------------------------------------------------------
|
|
75
|
+
-- form_endpoints: admin FOR ALL -> INSERT / UPDATE / DELETE, editor SELECT stays the only read
|
|
76
|
+
-- ---------------------------------------------------------------------------
|
|
77
|
+
DROP POLICY IF EXISTS form_endpoints_admin_write_policy ON public.form_endpoints;
|
|
78
|
+
DROP POLICY IF EXISTS form_endpoints_admin_insert_policy ON public.form_endpoints;
|
|
79
|
+
DROP POLICY IF EXISTS form_endpoints_admin_update_policy ON public.form_endpoints;
|
|
80
|
+
DROP POLICY IF EXISTS form_endpoints_admin_delete_policy ON public.form_endpoints;
|
|
81
|
+
|
|
82
|
+
CREATE POLICY form_endpoints_admin_insert_policy ON public.form_endpoints
|
|
83
|
+
FOR INSERT TO authenticated
|
|
84
|
+
WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);
|
|
85
|
+
|
|
86
|
+
CREATE POLICY form_endpoints_admin_update_policy ON public.form_endpoints
|
|
87
|
+
FOR UPDATE TO authenticated
|
|
88
|
+
USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
|
|
89
|
+
WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);
|
|
90
|
+
|
|
91
|
+
CREATE POLICY form_endpoints_admin_delete_policy ON public.form_endpoints
|
|
92
|
+
FOR DELETE TO authenticated
|
|
93
|
+
USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);
|
|
94
|
+
|
|
95
|
+
-- ---------------------------------------------------------------------------
|
|
96
|
+
-- SECURITY DEFINER trigger functions: not callable by API roles
|
|
97
|
+
-- ---------------------------------------------------------------------------
|
|
98
|
+
REVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated;
|
|
99
|
+
GRANT EXECUTE ON FUNCTION public.handle_new_user() TO service_role;
|
|
100
|
+
|
|
101
|
+
REVOKE EXECUTE ON FUNCTION public.update_product_ratings() FROM PUBLIC, anon, authenticated;
|
|
102
|
+
GRANT EXECUTE ON FUNCTION public.update_product_ratings() TO service_role;
|