@nextblock-cms/db 0.14.4 → 0.14.6
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/lib/supabase/server.d.ts +90 -0
- package/lib/supabase/types.d.ts +93 -0
- package/package.json +1 -1
- package/supabase/migrations/00000000000015_scheduled_publishing.sql +43 -0
- package/supabase/migrations/00000000000016_product_revisions_and_revision_baseline.sql +310 -0
- package/supabase/migrations/00000000000017_cortex_ai_mcp_server.sql +91 -0
package/lib/supabase/server.d.ts
CHANGED
|
@@ -725,6 +725,45 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
725
725
|
referencedColumns: ["id"];
|
|
726
726
|
}];
|
|
727
727
|
};
|
|
728
|
+
mcp_access_tokens: {
|
|
729
|
+
Row: {
|
|
730
|
+
created_at: string;
|
|
731
|
+
created_by: string | null;
|
|
732
|
+
expires_at: string | null;
|
|
733
|
+
id: string;
|
|
734
|
+
last_used_at: string | null;
|
|
735
|
+
name: string;
|
|
736
|
+
revoked_at: string | null;
|
|
737
|
+
scopes: string[];
|
|
738
|
+
token_hash: string;
|
|
739
|
+
token_prefix: string;
|
|
740
|
+
};
|
|
741
|
+
Insert: {
|
|
742
|
+
created_at?: string;
|
|
743
|
+
created_by?: string | null;
|
|
744
|
+
expires_at?: string | null;
|
|
745
|
+
id?: string;
|
|
746
|
+
last_used_at?: string | null;
|
|
747
|
+
name: string;
|
|
748
|
+
revoked_at?: string | null;
|
|
749
|
+
scopes?: string[];
|
|
750
|
+
token_hash: string;
|
|
751
|
+
token_prefix: string;
|
|
752
|
+
};
|
|
753
|
+
Update: {
|
|
754
|
+
created_at?: string;
|
|
755
|
+
created_by?: string | null;
|
|
756
|
+
expires_at?: string | null;
|
|
757
|
+
id?: string;
|
|
758
|
+
last_used_at?: string | null;
|
|
759
|
+
name?: string;
|
|
760
|
+
revoked_at?: string | null;
|
|
761
|
+
scopes?: string[];
|
|
762
|
+
token_hash?: string;
|
|
763
|
+
token_prefix?: string;
|
|
764
|
+
};
|
|
765
|
+
Relationships: [];
|
|
766
|
+
};
|
|
728
767
|
media: {
|
|
729
768
|
Row: {
|
|
730
769
|
blur_data_url: string | null;
|
|
@@ -1083,6 +1122,7 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1083
1122
|
language_id: number;
|
|
1084
1123
|
meta_description: string | null;
|
|
1085
1124
|
meta_title: string | null;
|
|
1125
|
+
published_at: string | null;
|
|
1086
1126
|
slug: string;
|
|
1087
1127
|
status: Database["public"]["Enums"]["page_status"];
|
|
1088
1128
|
title: string;
|
|
@@ -1099,6 +1139,7 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1099
1139
|
language_id: number;
|
|
1100
1140
|
meta_description?: string | null;
|
|
1101
1141
|
meta_title?: string | null;
|
|
1142
|
+
published_at?: string | null;
|
|
1102
1143
|
slug: string;
|
|
1103
1144
|
status?: Database["public"]["Enums"]["page_status"];
|
|
1104
1145
|
title: string;
|
|
@@ -1115,6 +1156,7 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1115
1156
|
language_id?: number;
|
|
1116
1157
|
meta_description?: string | null;
|
|
1117
1158
|
meta_title?: string | null;
|
|
1159
|
+
published_at?: string | null;
|
|
1118
1160
|
slug?: string;
|
|
1119
1161
|
status?: Database["public"]["Enums"]["page_status"];
|
|
1120
1162
|
title?: string;
|
|
@@ -1520,6 +1562,48 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1520
1562
|
referencedColumns: ["id"];
|
|
1521
1563
|
}];
|
|
1522
1564
|
};
|
|
1565
|
+
product_revisions: {
|
|
1566
|
+
Row: {
|
|
1567
|
+
author_id: string | null;
|
|
1568
|
+
content: import('./types').Json;
|
|
1569
|
+
created_at: string;
|
|
1570
|
+
id: number;
|
|
1571
|
+
product_id: string;
|
|
1572
|
+
revision_type: Database["public"]["Enums"]["revision_type"];
|
|
1573
|
+
version: number;
|
|
1574
|
+
};
|
|
1575
|
+
Insert: {
|
|
1576
|
+
author_id?: string | null;
|
|
1577
|
+
content: import('./types').Json;
|
|
1578
|
+
created_at?: string;
|
|
1579
|
+
id?: number;
|
|
1580
|
+
product_id: string;
|
|
1581
|
+
revision_type: Database["public"]["Enums"]["revision_type"];
|
|
1582
|
+
version: number;
|
|
1583
|
+
};
|
|
1584
|
+
Update: {
|
|
1585
|
+
author_id?: string | null;
|
|
1586
|
+
content?: import('./types').Json;
|
|
1587
|
+
created_at?: string;
|
|
1588
|
+
id?: number;
|
|
1589
|
+
product_id?: string;
|
|
1590
|
+
revision_type?: Database["public"]["Enums"]["revision_type"];
|
|
1591
|
+
version?: number;
|
|
1592
|
+
};
|
|
1593
|
+
Relationships: [{
|
|
1594
|
+
foreignKeyName: "product_revisions_author_id_fkey";
|
|
1595
|
+
columns: ["author_id"];
|
|
1596
|
+
isOneToOne: false;
|
|
1597
|
+
referencedRelation: "profiles";
|
|
1598
|
+
referencedColumns: ["id"];
|
|
1599
|
+
}, {
|
|
1600
|
+
foreignKeyName: "product_revisions_product_id_fkey";
|
|
1601
|
+
columns: ["product_id"];
|
|
1602
|
+
isOneToOne: false;
|
|
1603
|
+
referencedRelation: "products";
|
|
1604
|
+
referencedColumns: ["id"];
|
|
1605
|
+
}];
|
|
1606
|
+
};
|
|
1523
1607
|
product_variants: {
|
|
1524
1608
|
Row: {
|
|
1525
1609
|
created_at: string | null;
|
|
@@ -1613,6 +1697,7 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1613
1697
|
price: number;
|
|
1614
1698
|
prices: import('./types').Json;
|
|
1615
1699
|
product_type: string;
|
|
1700
|
+
published_at: string | null;
|
|
1616
1701
|
sale_end_at: string | null;
|
|
1617
1702
|
sale_price: number | null;
|
|
1618
1703
|
sale_prices: import('./types').Json | null;
|
|
@@ -1632,6 +1717,7 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1632
1717
|
trial_requires_payment_method: boolean;
|
|
1633
1718
|
upc: string | null;
|
|
1634
1719
|
updated_at: string | null;
|
|
1720
|
+
version: number;
|
|
1635
1721
|
};
|
|
1636
1722
|
Insert: {
|
|
1637
1723
|
average_rating?: number;
|
|
@@ -1650,6 +1736,7 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1650
1736
|
price: number;
|
|
1651
1737
|
prices?: import('./types').Json;
|
|
1652
1738
|
product_type: string;
|
|
1739
|
+
published_at?: string | null;
|
|
1653
1740
|
sale_end_at?: string | null;
|
|
1654
1741
|
sale_price?: number | null;
|
|
1655
1742
|
sale_prices?: import('./types').Json | null;
|
|
@@ -1669,6 +1756,7 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1669
1756
|
trial_requires_payment_method?: boolean;
|
|
1670
1757
|
upc?: string | null;
|
|
1671
1758
|
updated_at?: string | null;
|
|
1759
|
+
version?: number;
|
|
1672
1760
|
};
|
|
1673
1761
|
Update: {
|
|
1674
1762
|
average_rating?: number;
|
|
@@ -1687,6 +1775,7 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1687
1775
|
price?: number;
|
|
1688
1776
|
prices?: import('./types').Json;
|
|
1689
1777
|
product_type?: string;
|
|
1778
|
+
published_at?: string | null;
|
|
1690
1779
|
sale_end_at?: string | null;
|
|
1691
1780
|
sale_price?: number | null;
|
|
1692
1781
|
sale_prices?: import('./types').Json | null;
|
|
@@ -1706,6 +1795,7 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1706
1795
|
trial_requires_payment_method?: boolean;
|
|
1707
1796
|
upc?: string | null;
|
|
1708
1797
|
updated_at?: string | null;
|
|
1798
|
+
version?: number;
|
|
1709
1799
|
};
|
|
1710
1800
|
Relationships: [{
|
|
1711
1801
|
foreignKeyName: "products_language_id_fkey";
|
package/lib/supabase/types.d.ts
CHANGED
|
@@ -749,6 +749,45 @@ export type Database = {
|
|
|
749
749
|
}
|
|
750
750
|
];
|
|
751
751
|
};
|
|
752
|
+
mcp_access_tokens: {
|
|
753
|
+
Row: {
|
|
754
|
+
created_at: string;
|
|
755
|
+
created_by: string | null;
|
|
756
|
+
expires_at: string | null;
|
|
757
|
+
id: string;
|
|
758
|
+
last_used_at: string | null;
|
|
759
|
+
name: string;
|
|
760
|
+
revoked_at: string | null;
|
|
761
|
+
scopes: string[];
|
|
762
|
+
token_hash: string;
|
|
763
|
+
token_prefix: string;
|
|
764
|
+
};
|
|
765
|
+
Insert: {
|
|
766
|
+
created_at?: string;
|
|
767
|
+
created_by?: string | null;
|
|
768
|
+
expires_at?: string | null;
|
|
769
|
+
id?: string;
|
|
770
|
+
last_used_at?: string | null;
|
|
771
|
+
name: string;
|
|
772
|
+
revoked_at?: string | null;
|
|
773
|
+
scopes?: string[];
|
|
774
|
+
token_hash: string;
|
|
775
|
+
token_prefix: string;
|
|
776
|
+
};
|
|
777
|
+
Update: {
|
|
778
|
+
created_at?: string;
|
|
779
|
+
created_by?: string | null;
|
|
780
|
+
expires_at?: string | null;
|
|
781
|
+
id?: string;
|
|
782
|
+
last_used_at?: string | null;
|
|
783
|
+
name?: string;
|
|
784
|
+
revoked_at?: string | null;
|
|
785
|
+
scopes?: string[];
|
|
786
|
+
token_hash?: string;
|
|
787
|
+
token_prefix?: string;
|
|
788
|
+
};
|
|
789
|
+
Relationships: [];
|
|
790
|
+
};
|
|
752
791
|
media: {
|
|
753
792
|
Row: {
|
|
754
793
|
blur_data_url: string | null;
|
|
@@ -1122,6 +1161,7 @@ export type Database = {
|
|
|
1122
1161
|
language_id: number;
|
|
1123
1162
|
meta_description: string | null;
|
|
1124
1163
|
meta_title: string | null;
|
|
1164
|
+
published_at: string | null;
|
|
1125
1165
|
slug: string;
|
|
1126
1166
|
status: Database["public"]["Enums"]["page_status"];
|
|
1127
1167
|
title: string;
|
|
@@ -1138,6 +1178,7 @@ export type Database = {
|
|
|
1138
1178
|
language_id: number;
|
|
1139
1179
|
meta_description?: string | null;
|
|
1140
1180
|
meta_title?: string | null;
|
|
1181
|
+
published_at?: string | null;
|
|
1141
1182
|
slug: string;
|
|
1142
1183
|
status?: Database["public"]["Enums"]["page_status"];
|
|
1143
1184
|
title: string;
|
|
@@ -1154,6 +1195,7 @@ export type Database = {
|
|
|
1154
1195
|
language_id?: number;
|
|
1155
1196
|
meta_description?: string | null;
|
|
1156
1197
|
meta_title?: string | null;
|
|
1198
|
+
published_at?: string | null;
|
|
1157
1199
|
slug?: string;
|
|
1158
1200
|
status?: Database["public"]["Enums"]["page_status"];
|
|
1159
1201
|
title?: string;
|
|
@@ -1583,6 +1625,51 @@ export type Database = {
|
|
|
1583
1625
|
}
|
|
1584
1626
|
];
|
|
1585
1627
|
};
|
|
1628
|
+
product_revisions: {
|
|
1629
|
+
Row: {
|
|
1630
|
+
author_id: string | null;
|
|
1631
|
+
content: Json;
|
|
1632
|
+
created_at: string;
|
|
1633
|
+
id: number;
|
|
1634
|
+
product_id: string;
|
|
1635
|
+
revision_type: Database["public"]["Enums"]["revision_type"];
|
|
1636
|
+
version: number;
|
|
1637
|
+
};
|
|
1638
|
+
Insert: {
|
|
1639
|
+
author_id?: string | null;
|
|
1640
|
+
content: Json;
|
|
1641
|
+
created_at?: string;
|
|
1642
|
+
id?: number;
|
|
1643
|
+
product_id: string;
|
|
1644
|
+
revision_type: Database["public"]["Enums"]["revision_type"];
|
|
1645
|
+
version: number;
|
|
1646
|
+
};
|
|
1647
|
+
Update: {
|
|
1648
|
+
author_id?: string | null;
|
|
1649
|
+
content?: Json;
|
|
1650
|
+
created_at?: string;
|
|
1651
|
+
id?: number;
|
|
1652
|
+
product_id?: string;
|
|
1653
|
+
revision_type?: Database["public"]["Enums"]["revision_type"];
|
|
1654
|
+
version?: number;
|
|
1655
|
+
};
|
|
1656
|
+
Relationships: [
|
|
1657
|
+
{
|
|
1658
|
+
foreignKeyName: "product_revisions_author_id_fkey";
|
|
1659
|
+
columns: ["author_id"];
|
|
1660
|
+
isOneToOne: false;
|
|
1661
|
+
referencedRelation: "profiles";
|
|
1662
|
+
referencedColumns: ["id"];
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
foreignKeyName: "product_revisions_product_id_fkey";
|
|
1666
|
+
columns: ["product_id"];
|
|
1667
|
+
isOneToOne: false;
|
|
1668
|
+
referencedRelation: "products";
|
|
1669
|
+
referencedColumns: ["id"];
|
|
1670
|
+
}
|
|
1671
|
+
];
|
|
1672
|
+
};
|
|
1586
1673
|
product_variants: {
|
|
1587
1674
|
Row: {
|
|
1588
1675
|
created_at: string | null;
|
|
@@ -1679,6 +1766,7 @@ export type Database = {
|
|
|
1679
1766
|
price: number;
|
|
1680
1767
|
prices: Json;
|
|
1681
1768
|
product_type: string;
|
|
1769
|
+
published_at: string | null;
|
|
1682
1770
|
sale_end_at: string | null;
|
|
1683
1771
|
sale_price: number | null;
|
|
1684
1772
|
sale_prices: Json | null;
|
|
@@ -1698,6 +1786,7 @@ export type Database = {
|
|
|
1698
1786
|
trial_requires_payment_method: boolean;
|
|
1699
1787
|
upc: string | null;
|
|
1700
1788
|
updated_at: string | null;
|
|
1789
|
+
version: number;
|
|
1701
1790
|
};
|
|
1702
1791
|
Insert: {
|
|
1703
1792
|
average_rating?: number;
|
|
@@ -1716,6 +1805,7 @@ export type Database = {
|
|
|
1716
1805
|
price: number;
|
|
1717
1806
|
prices?: Json;
|
|
1718
1807
|
product_type: string;
|
|
1808
|
+
published_at?: string | null;
|
|
1719
1809
|
sale_end_at?: string | null;
|
|
1720
1810
|
sale_price?: number | null;
|
|
1721
1811
|
sale_prices?: Json | null;
|
|
@@ -1735,6 +1825,7 @@ export type Database = {
|
|
|
1735
1825
|
trial_requires_payment_method?: boolean;
|
|
1736
1826
|
upc?: string | null;
|
|
1737
1827
|
updated_at?: string | null;
|
|
1828
|
+
version?: number;
|
|
1738
1829
|
};
|
|
1739
1830
|
Update: {
|
|
1740
1831
|
average_rating?: number;
|
|
@@ -1753,6 +1844,7 @@ export type Database = {
|
|
|
1753
1844
|
price?: number;
|
|
1754
1845
|
prices?: Json;
|
|
1755
1846
|
product_type?: string;
|
|
1847
|
+
published_at?: string | null;
|
|
1756
1848
|
sale_end_at?: string | null;
|
|
1757
1849
|
sale_price?: number | null;
|
|
1758
1850
|
sale_prices?: Json | null;
|
|
@@ -1772,6 +1864,7 @@ export type Database = {
|
|
|
1772
1864
|
trial_requires_payment_method?: boolean;
|
|
1773
1865
|
upc?: string | null;
|
|
1774
1866
|
updated_at?: string | null;
|
|
1867
|
+
version?: number;
|
|
1775
1868
|
};
|
|
1776
1869
|
Relationships: [
|
|
1777
1870
|
{
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- Scheduled publishing for pages and products.
|
|
2
|
+
--
|
|
3
|
+
-- Posts already support scheduling: `posts.published_at` exists and every public
|
|
4
|
+
-- read gates on `published_at IS NULL OR published_at <= now()`, so a row with
|
|
5
|
+
-- status='published' and a future date is withheld until the date passes. Pages
|
|
6
|
+
-- and products had no equivalent column, so "go live on Tuesday" was impossible
|
|
7
|
+
-- for them. This adds the same column with the same semantics.
|
|
8
|
+
--
|
|
9
|
+
-- Visibility is derived from the (status, published_at) PAIR — no new enum value:
|
|
10
|
+
--
|
|
11
|
+
-- status = draft/archived -> not public, whatever the date
|
|
12
|
+
-- status = published|active, published_at NULL -> public now
|
|
13
|
+
-- status = published|active, date <= now() -> public now
|
|
14
|
+
-- status = published|active, date > now() -> SCHEDULED (withheld)
|
|
15
|
+
--
|
|
16
|
+
-- Deriving "scheduled" instead of storing it keeps `page_status` unchanged (adding
|
|
17
|
+
-- an enum value can't be done inside a transaction with other DDL in Postgres) and
|
|
18
|
+
-- matches what posts have always done, so one code path covers all three types.
|
|
19
|
+
--
|
|
20
|
+
-- NULL is the safe default: every existing published row keeps rendering exactly as
|
|
21
|
+
-- before, so this migration needs no backfill and changes no current behavior.
|
|
22
|
+
--
|
|
23
|
+
-- Forward-only and idempotent.
|
|
24
|
+
|
|
25
|
+
ALTER TABLE public.pages
|
|
26
|
+
ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;
|
|
27
|
+
|
|
28
|
+
COMMENT ON COLUMN public.pages.published_at IS
|
|
29
|
+
'Optional go-live moment. NULL = live as soon as status is published. A future value withholds the page from public reads until it passes (status stays "published"; the CMS renders that pair as "Scheduled").';
|
|
30
|
+
|
|
31
|
+
ALTER TABLE public.products
|
|
32
|
+
ADD COLUMN IF NOT EXISTS published_at timestamp with time zone;
|
|
33
|
+
|
|
34
|
+
COMMENT ON COLUMN public.products.published_at IS
|
|
35
|
+
'Optional go-live moment. NULL = live as soon as status is active. A future value withholds the product from public reads until it passes (status stays "active"; the CMS renders that pair as "Scheduled").';
|
|
36
|
+
|
|
37
|
+
-- Public listing/index queries filter on the (status, published_at) pair together
|
|
38
|
+
-- (catalog, sitemap, page lookups), so a composite index serves them in one pass.
|
|
39
|
+
CREATE INDEX IF NOT EXISTS pages_status_published_at_idx
|
|
40
|
+
ON public.pages (status, published_at);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS products_status_published_at_idx
|
|
43
|
+
ON public.products (status, published_at);
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
-- 00000000000016_product_revisions_and_revision_baseline.sql
|
|
2
|
+
--
|
|
3
|
+
-- Revision History, part 1 of 2 (schema). The application-side rewrite lives in
|
|
4
|
+
-- apps/nextblock/app/cms/revisions/**.
|
|
5
|
+
--
|
|
6
|
+
-- Three things happen here:
|
|
7
|
+
--
|
|
8
|
+
-- 1. products.version — the monotonic counter the hybrid revision engine drives,
|
|
9
|
+
-- mirroring pages.version / posts.version.
|
|
10
|
+
--
|
|
11
|
+
-- 2. product_revisions — a structural mirror of page_revisions / post_revisions.
|
|
12
|
+
-- product_id is uuid (products.id is uuid, not bigint), and
|
|
13
|
+
-- writes are gated on is_admin() to match products_*_policy
|
|
14
|
+
-- rather than the ADMIN|WRITER pattern the page/post revision
|
|
15
|
+
-- tables use. A WRITER who could insert a revision but not
|
|
16
|
+
-- apply a restore would get a silent no-op restore, because
|
|
17
|
+
-- PostgREST returns no error for an UPDATE matching zero rows.
|
|
18
|
+
--
|
|
19
|
+
-- 3. Revision baseline — every page, post and product gets a real `snapshot` row to
|
|
20
|
+
-- restore to. Until now the CMS synthesised a fake "Initial
|
|
21
|
+
-- Version" entry in the UI whose Restore button resolved to
|
|
22
|
+
-- "current metadata + zero blocks" and wiped the content.
|
|
23
|
+
-- There is now an actual stored baseline instead.
|
|
24
|
+
--
|
|
25
|
+
-- Case A (version = 1, no revisions at all): the live row IS
|
|
26
|
+
-- version 1. This covers seeded content — 00000000000003
|
|
27
|
+
-- inserts every page and post at version 1 and writes no
|
|
28
|
+
-- revision rows — and everything authored since the CMS save
|
|
29
|
+
-- path stopped recording revisions. Snapshotting it at
|
|
30
|
+
-- version 1 is what makes "restore the original seeded page"
|
|
31
|
+
-- real for the first time.
|
|
32
|
+
--
|
|
33
|
+
-- Case B (version > 1 but no snapshot at or below it): the
|
|
34
|
+
-- true v1 is unrecoverable and is NOT fabricated. A snapshot
|
|
35
|
+
-- of the current state is stored at the current version so the
|
|
36
|
+
-- diff chain has a valid base and future restores resolve.
|
|
37
|
+
--
|
|
38
|
+
-- Forward-only, idempotent, and it modifies no existing row: every backfill is an
|
|
39
|
+
-- INSERT ... WHERE NOT EXISTS ... ON CONFLICT DO NOTHING.
|
|
40
|
+
|
|
41
|
+
-- ---------------------------------------------------------------------------
|
|
42
|
+
-- 1. products.version
|
|
43
|
+
-- ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
ALTER TABLE public.products
|
|
46
|
+
ADD COLUMN IF NOT EXISTS version integer DEFAULT 1 NOT NULL;
|
|
47
|
+
|
|
48
|
+
COMMENT ON COLUMN public.products.version IS 'Monotonic version number for hybrid revisions.';
|
|
49
|
+
|
|
50
|
+
-- ---------------------------------------------------------------------------
|
|
51
|
+
-- 2. product_revisions
|
|
52
|
+
-- ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS public.product_revisions (
|
|
55
|
+
id bigint NOT NULL,
|
|
56
|
+
product_id uuid NOT NULL,
|
|
57
|
+
author_id uuid,
|
|
58
|
+
version integer NOT NULL,
|
|
59
|
+
revision_type public.revision_type NOT NULL,
|
|
60
|
+
content jsonb NOT NULL,
|
|
61
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
COMMENT ON TABLE public.product_revisions IS 'Hybrid (snapshot/diff) revisions for products.';
|
|
65
|
+
COMMENT ON COLUMN public.product_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
|
|
66
|
+
|
|
67
|
+
DO $rb$
|
|
68
|
+
BEGIN
|
|
69
|
+
IF NOT EXISTS (
|
|
70
|
+
SELECT 1 FROM pg_attribute
|
|
71
|
+
WHERE attrelid = 'public.product_revisions'::regclass
|
|
72
|
+
AND attname = 'id'
|
|
73
|
+
AND attidentity <> ''
|
|
74
|
+
) THEN
|
|
75
|
+
ALTER TABLE public.product_revisions
|
|
76
|
+
ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
77
|
+
SEQUENCE NAME public.product_revisions_id_seq
|
|
78
|
+
START WITH 1
|
|
79
|
+
INCREMENT BY 1
|
|
80
|
+
NO MINVALUE
|
|
81
|
+
NO MAXVALUE
|
|
82
|
+
CACHE 1
|
|
83
|
+
);
|
|
84
|
+
END IF;
|
|
85
|
+
END $rb$;
|
|
86
|
+
|
|
87
|
+
DO $rb$
|
|
88
|
+
BEGIN
|
|
89
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint
|
|
90
|
+
WHERE conname = 'product_revisions_pkey'
|
|
91
|
+
AND conrelid = 'public.product_revisions'::regclass) THEN
|
|
92
|
+
ALTER TABLE ONLY public.product_revisions
|
|
93
|
+
ADD CONSTRAINT product_revisions_pkey PRIMARY KEY (id);
|
|
94
|
+
END IF;
|
|
95
|
+
END $rb$;
|
|
96
|
+
|
|
97
|
+
DO $rb$
|
|
98
|
+
BEGIN
|
|
99
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint
|
|
100
|
+
WHERE conname = 'product_revisions_product_version_key'
|
|
101
|
+
AND conrelid = 'public.product_revisions'::regclass) THEN
|
|
102
|
+
ALTER TABLE ONLY public.product_revisions
|
|
103
|
+
ADD CONSTRAINT product_revisions_product_version_key UNIQUE (product_id, version);
|
|
104
|
+
END IF;
|
|
105
|
+
END $rb$;
|
|
106
|
+
|
|
107
|
+
DO $rb$
|
|
108
|
+
BEGIN
|
|
109
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint
|
|
110
|
+
WHERE conname = 'product_revisions_author_id_fkey'
|
|
111
|
+
AND conrelid = 'public.product_revisions'::regclass) THEN
|
|
112
|
+
ALTER TABLE ONLY public.product_revisions
|
|
113
|
+
ADD CONSTRAINT product_revisions_author_id_fkey
|
|
114
|
+
FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;
|
|
115
|
+
END IF;
|
|
116
|
+
END $rb$;
|
|
117
|
+
|
|
118
|
+
DO $rb$
|
|
119
|
+
BEGIN
|
|
120
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint
|
|
121
|
+
WHERE conname = 'product_revisions_product_id_fkey'
|
|
122
|
+
AND conrelid = 'public.product_revisions'::regclass) THEN
|
|
123
|
+
ALTER TABLE ONLY public.product_revisions
|
|
124
|
+
ADD CONSTRAINT product_revisions_product_id_fkey
|
|
125
|
+
FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;
|
|
126
|
+
END IF;
|
|
127
|
+
END $rb$;
|
|
128
|
+
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_product_revisions_author_id
|
|
130
|
+
ON public.product_revisions USING btree (author_id);
|
|
131
|
+
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_product_revisions_product_id_version
|
|
133
|
+
ON public.product_revisions USING btree (product_id, version);
|
|
134
|
+
|
|
135
|
+
ALTER TABLE public.product_revisions ENABLE ROW LEVEL SECURITY;
|
|
136
|
+
|
|
137
|
+
DROP POLICY IF EXISTS product_revisions_read_policy ON public.product_revisions;
|
|
138
|
+
CREATE POLICY product_revisions_read_policy ON public.product_revisions
|
|
139
|
+
FOR SELECT TO authenticated USING (true);
|
|
140
|
+
|
|
141
|
+
DROP POLICY IF EXISTS product_revisions_insert_policy ON public.product_revisions;
|
|
142
|
+
CREATE POLICY product_revisions_insert_policy ON public.product_revisions
|
|
143
|
+
FOR INSERT TO authenticated
|
|
144
|
+
WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));
|
|
145
|
+
|
|
146
|
+
DROP POLICY IF EXISTS product_revisions_update_policy ON public.product_revisions;
|
|
147
|
+
CREATE POLICY product_revisions_update_policy ON public.product_revisions
|
|
148
|
+
FOR UPDATE TO authenticated
|
|
149
|
+
USING (((SELECT public.is_admin() AS is_admin) IS TRUE))
|
|
150
|
+
WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));
|
|
151
|
+
|
|
152
|
+
DROP POLICY IF EXISTS product_revisions_delete_policy ON public.product_revisions;
|
|
153
|
+
CREATE POLICY product_revisions_delete_policy ON public.product_revisions
|
|
154
|
+
FOR DELETE TO authenticated
|
|
155
|
+
USING (((SELECT public.is_admin() AS is_admin) IS TRUE));
|
|
156
|
+
|
|
157
|
+
GRANT ALL ON TABLE public.product_revisions TO anon;
|
|
158
|
+
GRANT ALL ON TABLE public.product_revisions TO authenticated;
|
|
159
|
+
GRANT ALL ON TABLE public.product_revisions TO service_role;
|
|
160
|
+
GRANT ALL ON SEQUENCE public.product_revisions_id_seq TO anon;
|
|
161
|
+
GRANT ALL ON SEQUENCE public.product_revisions_id_seq TO authenticated;
|
|
162
|
+
GRANT ALL ON SEQUENCE public.product_revisions_id_seq TO service_role;
|
|
163
|
+
|
|
164
|
+
-- ---------------------------------------------------------------------------
|
|
165
|
+
-- 3. Revision baseline backfill
|
|
166
|
+
--
|
|
167
|
+
-- The JSON shape must match FullPageContent / FullPostContent / FullProductContent
|
|
168
|
+
-- in apps/nextblock/app/cms/revisions/utils.ts exactly, or the first diff taken
|
|
169
|
+
-- against a baseline row will be full of phantom operations.
|
|
170
|
+
--
|
|
171
|
+
-- Timestamps are rendered with an explicit millisecond-precision UTC format so they
|
|
172
|
+
-- match JavaScript's Date#toISOString() ("2026-07-03T17:52:15.643Z"). Postgres'
|
|
173
|
+
-- default jsonb rendering of timestamptz ("2026-07-03T17:52:15.643901+00:00") would
|
|
174
|
+
-- differ from the value the application writes and produce a spurious diff on the
|
|
175
|
+
-- very next save.
|
|
176
|
+
-- ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
-- 3a. Pages
|
|
179
|
+
INSERT INTO public.page_revisions (page_id, author_id, version, revision_type, content)
|
|
180
|
+
SELECT
|
|
181
|
+
p.id,
|
|
182
|
+
NULL::uuid,
|
|
183
|
+
p.version,
|
|
184
|
+
'snapshot'::public.revision_type,
|
|
185
|
+
jsonb_build_object(
|
|
186
|
+
'meta', jsonb_build_object(
|
|
187
|
+
'title', p.title,
|
|
188
|
+
'slug', p.slug,
|
|
189
|
+
'language_id', p.language_id,
|
|
190
|
+
'status', p.status,
|
|
191
|
+
'meta_title', p.meta_title,
|
|
192
|
+
'meta_description', p.meta_description,
|
|
193
|
+
'custom_canonical', p.custom_canonical,
|
|
194
|
+
'published_at', to_char(p.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
|
|
195
|
+
'feature_image_id', p.feature_image_id
|
|
196
|
+
),
|
|
197
|
+
'blocks', COALESCE((
|
|
198
|
+
SELECT jsonb_agg(
|
|
199
|
+
jsonb_build_object(
|
|
200
|
+
'language_id', b.language_id,
|
|
201
|
+
'block_type', b.block_type,
|
|
202
|
+
'content', b.content,
|
|
203
|
+
'order', b."order"
|
|
204
|
+
) ORDER BY b."order" ASC, b.id ASC
|
|
205
|
+
)
|
|
206
|
+
FROM public.blocks b
|
|
207
|
+
WHERE b.page_id = p.id
|
|
208
|
+
), '[]'::jsonb)
|
|
209
|
+
)
|
|
210
|
+
FROM public.pages p
|
|
211
|
+
WHERE NOT EXISTS (
|
|
212
|
+
SELECT 1 FROM public.page_revisions r
|
|
213
|
+
WHERE r.page_id = p.id
|
|
214
|
+
AND r.revision_type = 'snapshot'
|
|
215
|
+
AND r.version <= p.version
|
|
216
|
+
)
|
|
217
|
+
ON CONFLICT (page_id, version) DO NOTHING;
|
|
218
|
+
|
|
219
|
+
-- 3b. Posts
|
|
220
|
+
INSERT INTO public.post_revisions (post_id, author_id, version, revision_type, content)
|
|
221
|
+
SELECT
|
|
222
|
+
po.id,
|
|
223
|
+
NULL::uuid,
|
|
224
|
+
po.version,
|
|
225
|
+
'snapshot'::public.revision_type,
|
|
226
|
+
jsonb_build_object(
|
|
227
|
+
'meta', jsonb_build_object(
|
|
228
|
+
'title', po.title,
|
|
229
|
+
'slug', po.slug,
|
|
230
|
+
'language_id', po.language_id,
|
|
231
|
+
'status', po.status,
|
|
232
|
+
'meta_title', po.meta_title,
|
|
233
|
+
'meta_description', po.meta_description,
|
|
234
|
+
'custom_canonical', po.custom_canonical,
|
|
235
|
+
'published_at', to_char(po.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
|
|
236
|
+
'feature_image_id', po.feature_image_id,
|
|
237
|
+
'label', po.label,
|
|
238
|
+
'excerpt', po.excerpt,
|
|
239
|
+
'subtitle', po.subtitle
|
|
240
|
+
),
|
|
241
|
+
'blocks', COALESCE((
|
|
242
|
+
SELECT jsonb_agg(
|
|
243
|
+
jsonb_build_object(
|
|
244
|
+
'language_id', b.language_id,
|
|
245
|
+
'block_type', b.block_type,
|
|
246
|
+
'content', b.content,
|
|
247
|
+
'order', b."order"
|
|
248
|
+
) ORDER BY b."order" ASC, b.id ASC
|
|
249
|
+
)
|
|
250
|
+
FROM public.blocks b
|
|
251
|
+
WHERE b.post_id = po.id
|
|
252
|
+
), '[]'::jsonb)
|
|
253
|
+
)
|
|
254
|
+
FROM public.posts po
|
|
255
|
+
WHERE NOT EXISTS (
|
|
256
|
+
SELECT 1 FROM public.post_revisions r
|
|
257
|
+
WHERE r.post_id = po.id
|
|
258
|
+
AND r.revision_type = 'snapshot'
|
|
259
|
+
AND r.version <= po.version
|
|
260
|
+
)
|
|
261
|
+
ON CONFLICT (post_id, version) DO NOTHING;
|
|
262
|
+
|
|
263
|
+
-- 3c. Products.
|
|
264
|
+
--
|
|
265
|
+
-- Content only. price/prices/sale_*/scheduled_*/stock/sku/average_rating/total_reviews
|
|
266
|
+
-- are deliberately excluded from the snapshot: pricing and inventory are mutated from
|
|
267
|
+
-- outside the editor (promotions, Freemius sync, order fulfilment), ratings are derived
|
|
268
|
+
-- aggregates, and inventory_items is keyed by bare SKU text with no FK to products — so
|
|
269
|
+
-- replaying commerce state on restore would reach rows the editor never touched.
|
|
270
|
+
-- Restoring a product restores its content, not its commerce state.
|
|
271
|
+
INSERT INTO public.product_revisions (product_id, author_id, version, revision_type, content)
|
|
272
|
+
SELECT
|
|
273
|
+
pr.id,
|
|
274
|
+
NULL::uuid,
|
|
275
|
+
pr.version,
|
|
276
|
+
'snapshot'::public.revision_type,
|
|
277
|
+
jsonb_build_object(
|
|
278
|
+
'meta', jsonb_build_object(
|
|
279
|
+
'title', pr.title,
|
|
280
|
+
'slug', pr.slug,
|
|
281
|
+
'language_id', pr.language_id,
|
|
282
|
+
'status', pr.status,
|
|
283
|
+
'meta_title', pr.meta_title,
|
|
284
|
+
'meta_description', pr.meta_description,
|
|
285
|
+
'custom_canonical', pr.custom_canonical,
|
|
286
|
+
'published_at', to_char(pr.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
|
|
287
|
+
'short_description', pr.short_description,
|
|
288
|
+
'description_json', pr.description_json
|
|
289
|
+
),
|
|
290
|
+
'blocks', COALESCE((
|
|
291
|
+
SELECT jsonb_agg(
|
|
292
|
+
jsonb_build_object(
|
|
293
|
+
'language_id', b.language_id,
|
|
294
|
+
'block_type', b.block_type,
|
|
295
|
+
'content', b.content,
|
|
296
|
+
'order', b."order"
|
|
297
|
+
) ORDER BY b."order" ASC, b.id ASC
|
|
298
|
+
)
|
|
299
|
+
FROM public.blocks b
|
|
300
|
+
WHERE b.product_id = pr.id
|
|
301
|
+
), '[]'::jsonb)
|
|
302
|
+
)
|
|
303
|
+
FROM public.products pr
|
|
304
|
+
WHERE NOT EXISTS (
|
|
305
|
+
SELECT 1 FROM public.product_revisions r
|
|
306
|
+
WHERE r.product_id = pr.id
|
|
307
|
+
AND r.revision_type = 'snapshot'
|
|
308
|
+
AND r.version <= pr.version
|
|
309
|
+
)
|
|
310
|
+
ON CONFLICT (product_id, version) DO NOTHING;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
-- Cortex AI MCP (Model Context Protocol) server access.
|
|
2
|
+
--
|
|
3
|
+
-- Adds the bearer-token store that gates /api/mcp, the endpoint that exposes the
|
|
4
|
+
-- Cortex AI tool registry to external MCP clients (Claude Code, Claude Desktop,
|
|
5
|
+
-- Cursor, VS Code). Two pieces:
|
|
6
|
+
--
|
|
7
|
+
-- 1. public.mcp_access_tokens — one row per issued token. We store ONLY the
|
|
8
|
+
-- SHA-256 hash of the token, never the token itself: the plaintext is shown
|
|
9
|
+
-- to the admin exactly once at mint time and is unrecoverable afterwards, so
|
|
10
|
+
-- a database leak cannot be replayed against the MCP endpoint. `token_prefix`
|
|
11
|
+
-- is the non-secret leading fragment kept purely so the UI can tell two tokens
|
|
12
|
+
-- apart in a list.
|
|
13
|
+
--
|
|
14
|
+
-- 2. cortex_ai_mcp_settings — a non-secret JSON site_settings row holding the
|
|
15
|
+
-- server on/off switch and the localhost-trust flag. It is added to all four
|
|
16
|
+
-- site_settings policies so only authenticated ADMINs can read or write it;
|
|
17
|
+
-- the MCP route itself reads it through the service-role client, which
|
|
18
|
+
-- bypasses RLS.
|
|
19
|
+
--
|
|
20
|
+
-- Forward-only. Recreates the four site_settings policies idempotently, preserving
|
|
21
|
+
-- every key already in each policy's sensitive array (note that
|
|
22
|
+
-- language_detection_settings stays anon-READABLE and so is absent from the SELECT
|
|
23
|
+
-- policy, exactly as migration 00000000000012 left it).
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS public.mcp_access_tokens (
|
|
26
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
27
|
+
name text NOT NULL,
|
|
28
|
+
-- Lowercase hex SHA-256 of the plaintext token. Unique so a lookup is a single
|
|
29
|
+
-- indexed equality probe and duplicate mints are impossible.
|
|
30
|
+
token_hash text NOT NULL UNIQUE,
|
|
31
|
+
-- Non-secret display fragment, e.g. "nbmcp_a1b2c3d4". Never enough to authenticate.
|
|
32
|
+
token_prefix text NOT NULL,
|
|
33
|
+
-- 'read' grants the read-only tools; 'write' additionally grants the mutating ones.
|
|
34
|
+
scopes text[] NOT NULL DEFAULT ARRAY['read', 'write']::text[],
|
|
35
|
+
created_by uuid REFERENCES auth.users (id) ON DELETE SET NULL,
|
|
36
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
37
|
+
last_used_at timestamptz,
|
|
38
|
+
expires_at timestamptz,
|
|
39
|
+
revoked_at timestamptz
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
COMMENT ON TABLE public.mcp_access_tokens IS
|
|
43
|
+
'Bearer tokens for the Cortex AI MCP server at /api/mcp. Stores SHA-256 hashes only; plaintext is displayed once at mint time.';
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS mcp_access_tokens_token_hash_idx
|
|
46
|
+
ON public.mcp_access_tokens (token_hash);
|
|
47
|
+
|
|
48
|
+
-- Orders the admin token list newest-first without a sort.
|
|
49
|
+
CREATE INDEX IF NOT EXISTS mcp_access_tokens_created_at_idx
|
|
50
|
+
ON public.mcp_access_tokens (created_at DESC);
|
|
51
|
+
|
|
52
|
+
ALTER TABLE public.mcp_access_tokens ENABLE ROW LEVEL SECURITY;
|
|
53
|
+
|
|
54
|
+
-- Tokens are credentials: admin-only, with no anon or WRITER access at all. The
|
|
55
|
+
-- MCP route verifies them with the service-role client, which bypasses RLS.
|
|
56
|
+
DROP POLICY IF EXISTS mcp_access_tokens_admin_select ON public.mcp_access_tokens;
|
|
57
|
+
CREATE POLICY mcp_access_tokens_admin_select ON public.mcp_access_tokens
|
|
58
|
+
FOR SELECT TO authenticated
|
|
59
|
+
USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);
|
|
60
|
+
|
|
61
|
+
DROP POLICY IF EXISTS mcp_access_tokens_admin_insert ON public.mcp_access_tokens;
|
|
62
|
+
CREATE POLICY mcp_access_tokens_admin_insert ON public.mcp_access_tokens
|
|
63
|
+
FOR INSERT TO authenticated
|
|
64
|
+
WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);
|
|
65
|
+
|
|
66
|
+
DROP POLICY IF EXISTS mcp_access_tokens_admin_update ON public.mcp_access_tokens;
|
|
67
|
+
CREATE POLICY mcp_access_tokens_admin_update ON public.mcp_access_tokens
|
|
68
|
+
FOR UPDATE TO authenticated
|
|
69
|
+
USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
|
|
70
|
+
WITH CHECK ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);
|
|
71
|
+
|
|
72
|
+
DROP POLICY IF EXISTS mcp_access_tokens_admin_delete ON public.mcp_access_tokens;
|
|
73
|
+
CREATE POLICY mcp_access_tokens_admin_delete ON public.mcp_access_tokens
|
|
74
|
+
FOR DELETE TO authenticated
|
|
75
|
+
USING ((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role);
|
|
76
|
+
|
|
77
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.mcp_access_tokens TO authenticated;
|
|
78
|
+
GRANT ALL ON public.mcp_access_tokens TO service_role;
|
|
79
|
+
|
|
80
|
+
-- Add cortex_ai_mcp_settings to the admin-only site_settings group (all four policies).
|
|
81
|
+
DROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;
|
|
82
|
+
CREATE POLICY site_settings_read_policy ON public.site_settings FOR SELECT USING (((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT auth.role() AS role) = 'authenticated'::text) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
|
|
83
|
+
|
|
84
|
+
DROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;
|
|
85
|
+
CREATE POLICY site_settings_insert_policy ON public.site_settings FOR INSERT TO authenticated WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
|
|
86
|
+
|
|
87
|
+
DROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;
|
|
88
|
+
CREATE POLICY site_settings_update_policy ON public.site_settings FOR UPDATE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role)))) WITH CHECK ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
|
|
89
|
+
|
|
90
|
+
DROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;
|
|
91
|
+
CREATE POLICY site_settings_delete_policy ON public.site_settings FOR DELETE TO authenticated USING ((((key <> ALL (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = ANY (ARRAY['ADMIN'::public.user_role, 'WRITER'::public.user_role]))) OR ((key = ANY (ARRAY['cortex_ai_openrouter_api_key'::text, 'bot_protection_secret'::text, 'email_secret'::text, 'payment_secret'::text, 'language_detection_settings'::text, 'cortex_ai_pexels_api_key'::text, 'cortex_ai_unsplash_access_key'::text, 'cortex_ai_mcp_settings'::text])) AND (( SELECT public.get_current_user_role() AS get_current_user_role) = 'ADMIN'::public.user_role))));
|