@fayz-ai/plugin-marketing 0.2.1 → 0.8.1
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/README.md +2 -0
- package/dist/MarketingContext.d.ts +2 -0
- package/dist/MarketingContext.d.ts.map +1 -1
- package/dist/MarketingPage.d.ts.map +1 -1
- package/dist/data/blogEntities.d.ts +28 -0
- package/dist/data/blogEntities.d.ts.map +1 -0
- package/dist/data/contentSupabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +6 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/index.cjs +630 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +636 -133
- package/dist/index.js.map +1 -1
- package/dist/integrations/ranklayer/connectorDef.d.ts +3 -0
- package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -0
- package/dist/integrations/ranklayer/data/supabase.d.ts +16 -0
- package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -0
- package/dist/integrations/ranklayer/types.d.ts +16 -0
- package/dist/integrations/ranklayer/types.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +9 -3
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/schema/index.d.ts +44 -44
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema.cjs +9 -9
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +9 -9
- package/dist/schema.js.map +1 -1
- package/dist/views/blog/BlogAdminView.d.ts +5 -0
- package/dist/views/blog/BlogAdminView.d.ts.map +1 -0
- package/dist/views/blog/BlogEntityForm.d.ts +11 -0
- package/dist/views/blog/BlogEntityForm.d.ts.map +1 -0
- package/dist/views/blog/useEntityCrud.d.ts +29 -0
- package/dist/views/blog/useEntityCrud.d.ts.map +1 -0
- package/package.json +8 -5
- package/src/MarketingContext.tsx +2 -0
- package/src/MarketingPage.tsx +22 -1
- package/src/data/blogEntities.tsx +91 -0
- package/src/data/contentSupabase.ts +18 -17
- package/src/data/tables.ts +7 -0
- package/src/index.ts +27 -3
- package/src/integrations/ranklayer/RANKLAYER.md +40 -0
- package/src/integrations/ranklayer/connectorDef.tsx +98 -0
- package/src/integrations/ranklayer/data/supabase.ts +106 -0
- package/src/integrations/ranklayer/types.ts +16 -0
- package/src/migrations/000_plg_rename.sql +15 -0
- package/src/migrations/001_content_planner.sql +48 -48
- package/src/migrations/002_multi_platform.sql +5 -5
- package/src/migrations/003_recording_ops.sql +10 -10
- package/src/migrations/004_ranklayer.sql +50 -0
- package/src/migrations/index.ts +152 -74
- package/src/presets.ts +5 -3
- package/src/schema/index.ts +9 -9
- package/src/views/blog/BlogAdminView.tsx +99 -0
- package/src/views/blog/BlogEntityForm.tsx +92 -0
- package/src/views/blog/useEntityCrud.ts +50 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// RankLayer as a unified ConnectorDefinition, surfaced in Marketing → settings
|
|
2
|
+
// → Integrações (rendered by the shared ConnectorsHub). SCAFFOLD: the hub gives
|
|
3
|
+
// us the API-key form + Test/Save; the ExtraPanel below explains that automated
|
|
4
|
+
// SEO content sync is being prepared. The real sync lands via an external PR —
|
|
5
|
+
// see ./RANKLAYER.md.
|
|
6
|
+
import React, { useEffect, useState } from 'react'
|
|
7
|
+
import { Sparkles, History, CheckCircle2, AlertCircle } from 'lucide-react'
|
|
8
|
+
import type { ConnectorDefinition } from '@fayz-ai/core'
|
|
9
|
+
import { createRankLayerProvider } from './data/supabase'
|
|
10
|
+
import type { RankLayerIntegration, RankLayerSyncLogEntry } from './types'
|
|
11
|
+
|
|
12
|
+
const provider = createRankLayerProvider()
|
|
13
|
+
|
|
14
|
+
function RankLayerExtraPanel() {
|
|
15
|
+
const [integration, setIntegration] = useState<RankLayerIntegration | null>(null)
|
|
16
|
+
const [log, setLog] = useState<RankLayerSyncLogEntry[]>([])
|
|
17
|
+
const [loaded, setLoaded] = useState(false)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let cancelled = false
|
|
21
|
+
;(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const found = await provider.getIntegration()
|
|
24
|
+
if (cancelled) return
|
|
25
|
+
setIntegration(found)
|
|
26
|
+
if (found) setLog(await provider.getSyncLog(found.id))
|
|
27
|
+
} catch {
|
|
28
|
+
/* not connected / no client — nothing to show */
|
|
29
|
+
} finally {
|
|
30
|
+
if (!cancelled) setLoaded(true)
|
|
31
|
+
}
|
|
32
|
+
})()
|
|
33
|
+
return () => { cancelled = true }
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
if (!loaded || !integration) return null // nothing until the connection is saved
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-4 border-t pt-4">
|
|
40
|
+
<div className="flex items-start gap-3 rounded-md border border-dashed p-3">
|
|
41
|
+
<Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
42
|
+
<div className="space-y-1">
|
|
43
|
+
<h4 className="text-sm font-semibold">Sincronização de conteúdo — em breve</h4>
|
|
44
|
+
<p className="text-xs text-muted-foreground">
|
|
45
|
+
A publicação automática de páginas e artigos de SEO com a RankLayer está em preparação.
|
|
46
|
+
{integration.siteDomain ? ` Domínio configurado: ${integration.siteDomain}.` : ''} Assim que a
|
|
47
|
+
integração estiver ativa, o histórico de sincronizações aparece aqui.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{log.length > 0 && (
|
|
53
|
+
<div className="space-y-2">
|
|
54
|
+
<div className="flex items-center gap-2">
|
|
55
|
+
<History className="h-4 w-4 text-muted-foreground" />
|
|
56
|
+
<h4 className="text-sm font-semibold">Histórico</h4>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="divide-y rounded-md border text-sm">
|
|
59
|
+
{log.map((r) => (
|
|
60
|
+
<div key={r.id} className="flex items-center gap-3 px-3 py-2">
|
|
61
|
+
<span className="text-xs text-muted-foreground">{new Date(r.createdAt).toLocaleString('pt-BR')}</span>
|
|
62
|
+
<span className="ml-auto text-xs">{r.written}/{r.fetched} publicadas</span>
|
|
63
|
+
{r.status === 'success'
|
|
64
|
+
? <CheckCircle2 className="h-3.5 w-3.5 text-success" />
|
|
65
|
+
: <AlertCircle className="h-3.5 w-3.5 text-warning" />}
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const ranklayerConnectorDef: ConnectorDefinition = {
|
|
76
|
+
id: 'ranklayer',
|
|
77
|
+
hostPluginId: 'marketing',
|
|
78
|
+
name: 'RankLayer',
|
|
79
|
+
description: 'Publicação de conteúdo e páginas de SEO por serviço/bairro para ser encontrado no Google e em assistentes de IA.',
|
|
80
|
+
icon: 'Search',
|
|
81
|
+
authKind: 'api-key',
|
|
82
|
+
fields: [
|
|
83
|
+
{ key: 'apiKey', label: 'API Key', type: 'password', placeholder: '••••••••' },
|
|
84
|
+
{ key: 'siteDomain', label: 'Domínio do site', type: 'text', placeholder: 'exemplo.com.br' },
|
|
85
|
+
],
|
|
86
|
+
async getStatus() {
|
|
87
|
+
try {
|
|
88
|
+
const found = await provider.getIntegration()
|
|
89
|
+
return { connected: !!found?.apiKey, detail: found?.siteDomain }
|
|
90
|
+
} catch {
|
|
91
|
+
return { connected: false }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
testConnection: (values) => provider.testConnection({ apiKey: values.apiKey }),
|
|
95
|
+
saveConnection: async (values) => { await provider.saveIntegration({ apiKey: values.apiKey, siteDomain: values.siteDomain }) },
|
|
96
|
+
disconnect: () => provider.disconnect(),
|
|
97
|
+
ExtraPanel: RankLayerExtraPanel,
|
|
98
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
|
|
2
|
+
import type { TestConnectionResult } from '@fayz-ai/core'
|
|
3
|
+
import type { RankLayerIntegration, RankLayerSyncLogEntry } from '../types'
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// RankLayer connector — control-plane client (SCAFFOLD).
|
|
7
|
+
//
|
|
8
|
+
// Reads/writes the connection row (plg_marketing_ranklayer_integrations) and
|
|
9
|
+
// reads the sync log. `testConnection` and `sync` are stubbed: RankLayer has no
|
|
10
|
+
// public API wired yet, so the real work (an edge function calling RankLayer)
|
|
11
|
+
// lands via an external PR — see ../RANKLAYER.md. Everything here is safe to
|
|
12
|
+
// ship: it only persists the tenant's API key + domain and surfaces status.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const INTEGRATIONS = 'plg_marketing_ranklayer_integrations'
|
|
16
|
+
const SYNC_LOG = 'plg_marketing_ranklayer_sync_log'
|
|
17
|
+
|
|
18
|
+
function getClient() {
|
|
19
|
+
const supabase = getSupabaseClientOptional() as { from: (t: string) => any } | null
|
|
20
|
+
if (!supabase) throw new Error('[ranklayer] Supabase not initialized')
|
|
21
|
+
return supabase
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function requireTenantId(): string {
|
|
25
|
+
const tenantId = getActiveTenantId()
|
|
26
|
+
if (!tenantId) throw new Error('[ranklayer] No active tenant')
|
|
27
|
+
return tenantId
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toIntegration(row: Record<string, any>): RankLayerIntegration {
|
|
31
|
+
return {
|
|
32
|
+
id: row.id,
|
|
33
|
+
apiKey: row.api_key ?? undefined,
|
|
34
|
+
siteDomain: row.site_domain ?? undefined,
|
|
35
|
+
active: row.active ?? true,
|
|
36
|
+
lastSyncAt: row.last_sync_at ?? undefined,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createRankLayerProvider() {
|
|
41
|
+
return {
|
|
42
|
+
async getIntegration(): Promise<RankLayerIntegration | null> {
|
|
43
|
+
const tenantId = getActiveTenantId()
|
|
44
|
+
if (!tenantId) return null
|
|
45
|
+
const { data } = await getClient()
|
|
46
|
+
.from(INTEGRATIONS)
|
|
47
|
+
.select('*')
|
|
48
|
+
.eq('tenant_id', tenantId)
|
|
49
|
+
.maybeSingle()
|
|
50
|
+
return data ? toIntegration(data) : null
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async saveIntegration(values: { apiKey: string; siteDomain?: string }): Promise<RankLayerIntegration> {
|
|
54
|
+
const tenantId = requireTenantId()
|
|
55
|
+
const payload = {
|
|
56
|
+
tenant_id: tenantId,
|
|
57
|
+
api_key: values.apiKey,
|
|
58
|
+
site_domain: values.siteDomain ?? null,
|
|
59
|
+
active: true,
|
|
60
|
+
updated_at: new Date().toISOString(),
|
|
61
|
+
}
|
|
62
|
+
// Upsert on the unique tenant_id.
|
|
63
|
+
const { data, error } = await getClient()
|
|
64
|
+
.from(INTEGRATIONS)
|
|
65
|
+
.upsert(payload, { onConflict: 'tenant_id' })
|
|
66
|
+
.select('*')
|
|
67
|
+
.single()
|
|
68
|
+
if (error) throw error
|
|
69
|
+
return toIntegration(data)
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async disconnect(): Promise<void> {
|
|
73
|
+
const tenantId = getActiveTenantId()
|
|
74
|
+
if (!tenantId) return
|
|
75
|
+
await getClient().from(INTEGRATIONS).delete().eq('tenant_id', tenantId)
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// SCAFFOLD: no RankLayer API yet — accept any non-empty key so the connect
|
|
79
|
+
// flow can be exercised. Replace with a real credential check in the PR.
|
|
80
|
+
async testConnection(values: { apiKey: string }): Promise<TestConnectionResult> {
|
|
81
|
+
if (!values.apiKey || values.apiKey.trim().length < 8) {
|
|
82
|
+
return { ok: false, message: 'Informe uma API Key válida da RankLayer.' }
|
|
83
|
+
}
|
|
84
|
+
return { ok: true, message: 'Chave aceita (validação real pendente — integração em preparação).' }
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async getSyncLog(integrationId: string): Promise<RankLayerSyncLogEntry[]> {
|
|
88
|
+
const { data } = await getClient()
|
|
89
|
+
.from(SYNC_LOG)
|
|
90
|
+
.select('*')
|
|
91
|
+
.eq('integration_id', integrationId)
|
|
92
|
+
.order('created_at', { ascending: false })
|
|
93
|
+
.limit(20)
|
|
94
|
+
return (data ?? []).map((r: Record<string, any>) => ({
|
|
95
|
+
id: r.id,
|
|
96
|
+
createdAt: r.created_at,
|
|
97
|
+
status: r.status,
|
|
98
|
+
message: r.message ?? undefined,
|
|
99
|
+
fetched: r.fetched ?? 0,
|
|
100
|
+
written: r.written ?? 0,
|
|
101
|
+
}))
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type RankLayerProvider = ReturnType<typeof createRankLayerProvider>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RankLayerIntegration {
|
|
2
|
+
id: string
|
|
3
|
+
apiKey?: string
|
|
4
|
+
siteDomain?: string
|
|
5
|
+
active: boolean
|
|
6
|
+
lastSyncAt?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface RankLayerSyncLogEntry {
|
|
10
|
+
id: string
|
|
11
|
+
createdAt: string
|
|
12
|
+
status: 'success' | 'partial' | 'error'
|
|
13
|
+
message?: string
|
|
14
|
+
fetched: number
|
|
15
|
+
written: number
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- 000_plg_rename.sql — rename legacy marketing tables to plg_marketing_* for pools
|
|
2
|
+
-- provisioned before the industry-pool rename. Guarded: fires only when legacy
|
|
3
|
+
-- name exists and target does not, so fresh pools skip every branch.
|
|
4
|
+
DO $$
|
|
5
|
+
BEGIN
|
|
6
|
+
IF to_regclass('public.mkt_social_accounts') IS NOT NULL AND to_regclass('public.plg_marketing_social_accounts') IS NULL THEN
|
|
7
|
+
ALTER TABLE public.mkt_social_accounts RENAME TO plg_marketing_social_accounts;
|
|
8
|
+
END IF;
|
|
9
|
+
IF to_regclass('public.mkt_content_plans') IS NOT NULL AND to_regclass('public.plg_marketing_content_plans') IS NULL THEN
|
|
10
|
+
ALTER TABLE public.mkt_content_plans RENAME TO plg_marketing_content_plans;
|
|
11
|
+
END IF;
|
|
12
|
+
IF to_regclass('public.mkt_content_posts') IS NOT NULL AND to_regclass('public.plg_marketing_content_posts') IS NULL THEN
|
|
13
|
+
ALTER TABLE public.mkt_content_posts RENAME TO plg_marketing_content_posts;
|
|
14
|
+
END IF;
|
|
15
|
+
END $$;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
-- Marketing Plugin: Content Planner
|
|
2
|
-
-- Prefix:
|
|
2
|
+
-- Prefix: plg_marketing_
|
|
3
3
|
-- Social accounts + content plans + posts (Notion-style markdown pages).
|
|
4
4
|
|
|
5
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
5
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_social_accounts (
|
|
6
6
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
7
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
7
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
8
8
|
name text NOT NULL,
|
|
9
9
|
handle text,
|
|
10
10
|
platform text NOT NULL DEFAULT 'instagram'
|
|
@@ -12,13 +12,13 @@ CREATE TABLE IF NOT EXISTS public.mkt_social_accounts (
|
|
|
12
12
|
is_active boolean NOT NULL DEFAULT true,
|
|
13
13
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
14
14
|
);
|
|
15
|
-
ALTER TABLE public.
|
|
16
|
-
CREATE INDEX IF NOT EXISTS
|
|
15
|
+
ALTER TABLE public.plg_marketing_social_accounts ENABLE ROW LEVEL SECURITY;
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_social_accounts_tenant ON public.plg_marketing_social_accounts(tenant_id);
|
|
17
17
|
|
|
18
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
18
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_content_plans (
|
|
19
19
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
20
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
21
|
-
account_id uuid NOT NULL REFERENCES public.
|
|
20
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
21
|
+
account_id uuid NOT NULL REFERENCES public.plg_marketing_social_accounts(id) ON DELETE CASCADE,
|
|
22
22
|
name text NOT NULL,
|
|
23
23
|
status text NOT NULL DEFAULT 'active'
|
|
24
24
|
CHECK (status IN ('draft', 'active', 'archived')),
|
|
@@ -33,14 +33,14 @@ CREATE TABLE IF NOT EXISTS public.mkt_content_plans (
|
|
|
33
33
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
34
34
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
35
35
|
);
|
|
36
|
-
ALTER TABLE public.
|
|
37
|
-
CREATE INDEX IF NOT EXISTS
|
|
38
|
-
CREATE INDEX IF NOT EXISTS
|
|
36
|
+
ALTER TABLE public.plg_marketing_content_plans ENABLE ROW LEVEL SECURITY;
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_tenant ON public.plg_marketing_content_plans(tenant_id);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_plans_account ON public.plg_marketing_content_plans(account_id);
|
|
39
39
|
|
|
40
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
40
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_content_posts (
|
|
41
41
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
42
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
43
|
-
plan_id uuid NOT NULL REFERENCES public.
|
|
42
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
43
|
+
plan_id uuid NOT NULL REFERENCES public.plg_marketing_content_plans(id) ON DELETE CASCADE,
|
|
44
44
|
week_number integer NOT NULL DEFAULT 1,
|
|
45
45
|
position integer NOT NULL DEFAULT 0,
|
|
46
46
|
title text NOT NULL,
|
|
@@ -55,40 +55,40 @@ CREATE TABLE IF NOT EXISTS public.mkt_content_posts (
|
|
|
55
55
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
56
56
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
57
57
|
);
|
|
58
|
-
ALTER TABLE public.
|
|
59
|
-
CREATE INDEX IF NOT EXISTS
|
|
60
|
-
CREATE INDEX IF NOT EXISTS
|
|
61
|
-
CREATE INDEX IF NOT EXISTS
|
|
58
|
+
ALTER TABLE public.plg_marketing_content_posts ENABLE ROW LEVEL SECURITY;
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_tenant ON public.plg_marketing_content_posts(tenant_id);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_plan ON public.plg_marketing_content_posts(plan_id, week_number, position);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_content_posts_status ON public.plg_marketing_content_posts(tenant_id, status);
|
|
62
62
|
|
|
63
|
-
-- RLS policies for
|
|
64
|
-
DROP POLICY IF EXISTS
|
|
65
|
-
DROP POLICY IF EXISTS
|
|
66
|
-
DROP POLICY IF EXISTS
|
|
67
|
-
DROP POLICY IF EXISTS
|
|
68
|
-
CREATE POLICY
|
|
69
|
-
CREATE POLICY
|
|
70
|
-
CREATE POLICY
|
|
71
|
-
CREATE POLICY
|
|
72
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.
|
|
63
|
+
-- RLS policies for plg_marketing_social_accounts
|
|
64
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts;
|
|
65
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts;
|
|
66
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts;
|
|
67
|
+
DROP POLICY IF EXISTS plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts;
|
|
68
|
+
CREATE POLICY plg_marketing_social_accounts_select ON public.plg_marketing_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
69
|
+
CREATE POLICY plg_marketing_social_accounts_insert ON public.plg_marketing_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
70
|
+
CREATE POLICY plg_marketing_social_accounts_update ON public.plg_marketing_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
71
|
+
CREATE POLICY plg_marketing_social_accounts_delete ON public.plg_marketing_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
72
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_social_accounts TO authenticated;
|
|
73
73
|
|
|
74
|
-
-- RLS policies for
|
|
75
|
-
DROP POLICY IF EXISTS
|
|
76
|
-
DROP POLICY IF EXISTS
|
|
77
|
-
DROP POLICY IF EXISTS
|
|
78
|
-
DROP POLICY IF EXISTS
|
|
79
|
-
CREATE POLICY
|
|
80
|
-
CREATE POLICY
|
|
81
|
-
CREATE POLICY
|
|
82
|
-
CREATE POLICY
|
|
83
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.
|
|
74
|
+
-- RLS policies for plg_marketing_content_plans
|
|
75
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_select ON public.plg_marketing_content_plans;
|
|
76
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_insert ON public.plg_marketing_content_plans;
|
|
77
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_update ON public.plg_marketing_content_plans;
|
|
78
|
+
DROP POLICY IF EXISTS plg_marketing_content_plans_delete ON public.plg_marketing_content_plans;
|
|
79
|
+
CREATE POLICY plg_marketing_content_plans_select ON public.plg_marketing_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
80
|
+
CREATE POLICY plg_marketing_content_plans_insert ON public.plg_marketing_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
81
|
+
CREATE POLICY plg_marketing_content_plans_update ON public.plg_marketing_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
82
|
+
CREATE POLICY plg_marketing_content_plans_delete ON public.plg_marketing_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
83
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_plans TO authenticated;
|
|
84
84
|
|
|
85
|
-
-- RLS policies for
|
|
86
|
-
DROP POLICY IF EXISTS
|
|
87
|
-
DROP POLICY IF EXISTS
|
|
88
|
-
DROP POLICY IF EXISTS
|
|
89
|
-
DROP POLICY IF EXISTS
|
|
90
|
-
CREATE POLICY
|
|
91
|
-
CREATE POLICY
|
|
92
|
-
CREATE POLICY
|
|
93
|
-
CREATE POLICY
|
|
94
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.
|
|
85
|
+
-- RLS policies for plg_marketing_content_posts
|
|
86
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_select ON public.plg_marketing_content_posts;
|
|
87
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_insert ON public.plg_marketing_content_posts;
|
|
88
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_update ON public.plg_marketing_content_posts;
|
|
89
|
+
DROP POLICY IF EXISTS plg_marketing_content_posts_delete ON public.plg_marketing_content_posts;
|
|
90
|
+
CREATE POLICY plg_marketing_content_posts_select ON public.plg_marketing_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
91
|
+
CREATE POLICY plg_marketing_content_posts_insert ON public.plg_marketing_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
92
|
+
CREATE POLICY plg_marketing_content_posts_update ON public.plg_marketing_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
93
|
+
CREATE POLICY plg_marketing_content_posts_delete ON public.plg_marketing_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
94
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_content_posts TO authenticated;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
-- YouTube, TikTok, ...). Posts optionally narrow the target platforms; an
|
|
4
4
|
-- empty array means "inherit the account's platforms".
|
|
5
5
|
|
|
6
|
-
ALTER TABLE public.
|
|
6
|
+
ALTER TABLE public.plg_marketing_social_accounts
|
|
7
7
|
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{instagram}';
|
|
8
8
|
|
|
9
9
|
-- Backfill from the legacy single-platform column, then drop it (also drops
|
|
@@ -12,14 +12,14 @@ DO $$
|
|
|
12
12
|
BEGIN
|
|
13
13
|
IF EXISTS (
|
|
14
14
|
SELECT 1 FROM information_schema.columns
|
|
15
|
-
WHERE table_schema = 'public' AND table_name = '
|
|
15
|
+
WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
|
|
16
16
|
) THEN
|
|
17
|
-
UPDATE public.
|
|
17
|
+
UPDATE public.plg_marketing_social_accounts
|
|
18
18
|
SET platforms = ARRAY[platform]
|
|
19
19
|
WHERE platform IS NOT NULL AND platforms = '{instagram}';
|
|
20
|
-
ALTER TABLE public.
|
|
20
|
+
ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
|
|
21
21
|
END IF;
|
|
22
22
|
END $$;
|
|
23
23
|
|
|
24
|
-
ALTER TABLE public.
|
|
24
|
+
ALTER TABLE public.plg_marketing_content_posts
|
|
25
25
|
ADD COLUMN IF NOT EXISTS platforms text[] NOT NULL DEFAULT '{}';
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
-- companion for recording day. media_url: uploaded asset (static posts get a
|
|
4
4
|
-- caption + final art instead of a script).
|
|
5
5
|
|
|
6
|
-
ALTER TABLE public.
|
|
6
|
+
ALTER TABLE public.plg_marketing_content_posts
|
|
7
7
|
ADD COLUMN IF NOT EXISTS checklist jsonb NOT NULL DEFAULT '[]';
|
|
8
8
|
|
|
9
|
-
ALTER TABLE public.
|
|
9
|
+
ALTER TABLE public.plg_marketing_content_posts
|
|
10
10
|
ADD COLUMN IF NOT EXISTS media_url text;
|
|
11
11
|
|
|
12
12
|
-- Public media bucket for content assets (mirrors the avatars-bucket pattern:
|
|
@@ -15,11 +15,11 @@ INSERT INTO storage.buckets (id, name, public)
|
|
|
15
15
|
VALUES ('mkt-media', 'mkt-media', true)
|
|
16
16
|
ON CONFLICT (id) DO NOTHING;
|
|
17
17
|
|
|
18
|
-
DROP POLICY IF EXISTS
|
|
19
|
-
DROP POLICY IF EXISTS
|
|
20
|
-
DROP POLICY IF EXISTS
|
|
21
|
-
DROP POLICY IF EXISTS
|
|
22
|
-
CREATE POLICY
|
|
23
|
-
CREATE POLICY
|
|
24
|
-
CREATE POLICY
|
|
25
|
-
CREATE POLICY
|
|
18
|
+
DROP POLICY IF EXISTS plg_marketing_media_read ON storage.objects;
|
|
19
|
+
DROP POLICY IF EXISTS plg_marketing_media_insert ON storage.objects;
|
|
20
|
+
DROP POLICY IF EXISTS plg_marketing_media_update ON storage.objects;
|
|
21
|
+
DROP POLICY IF EXISTS plg_marketing_media_delete ON storage.objects;
|
|
22
|
+
CREATE POLICY plg_marketing_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
|
|
23
|
+
CREATE POLICY plg_marketing_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
|
|
24
|
+
CREATE POLICY plg_marketing_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
|
|
25
|
+
CREATE POLICY plg_marketing_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- Marketing Plugin: RankLayer integration (SEO content publishing).
|
|
2
|
+
-- Control-plane state for the RankLayer connector rendered in Marketing →
|
|
3
|
+
-- Integrações. Prefix: plg_marketing_ranklayer_. Scaffold: the real sync
|
|
4
|
+
-- (edge function + RankLayer API calls) lands via an external PR — see
|
|
5
|
+
-- src/integrations/ranklayer/RANKLAYER.md.
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_integrations (
|
|
8
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
9
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
10
|
+
api_key text,
|
|
11
|
+
site_domain text,
|
|
12
|
+
active boolean NOT NULL DEFAULT true,
|
|
13
|
+
last_sync_at timestamptz,
|
|
14
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
15
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
16
|
+
);
|
|
17
|
+
ALTER TABLE public.plg_marketing_ranklayer_integrations ENABLE ROW LEVEL SECURITY;
|
|
18
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_marketing_ranklayer_integrations_tenant
|
|
19
|
+
ON public.plg_marketing_ranklayer_integrations(tenant_id);
|
|
20
|
+
|
|
21
|
+
CREATE TABLE IF NOT EXISTS public.plg_marketing_ranklayer_sync_log (
|
|
22
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
23
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
24
|
+
integration_id uuid REFERENCES public.plg_marketing_ranklayer_integrations(id) ON DELETE CASCADE,
|
|
25
|
+
status text NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'error')),
|
|
26
|
+
message text,
|
|
27
|
+
fetched integer NOT NULL DEFAULT 0,
|
|
28
|
+
written integer NOT NULL DEFAULT 0,
|
|
29
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
30
|
+
);
|
|
31
|
+
ALTER TABLE public.plg_marketing_ranklayer_sync_log ENABLE ROW LEVEL SECURITY;
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_plg_marketing_ranklayer_sync_log_tenant
|
|
33
|
+
ON public.plg_marketing_ranklayer_sync_log(tenant_id, created_at DESC);
|
|
34
|
+
|
|
35
|
+
-- RLS: authenticated tenant CRUD
|
|
36
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations;
|
|
37
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations;
|
|
38
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations;
|
|
39
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations;
|
|
40
|
+
CREATE POLICY plg_marketing_ranklayer_integrations_select ON public.plg_marketing_ranklayer_integrations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
41
|
+
CREATE POLICY plg_marketing_ranklayer_integrations_insert ON public.plg_marketing_ranklayer_integrations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
42
|
+
CREATE POLICY plg_marketing_ranklayer_integrations_update ON public.plg_marketing_ranklayer_integrations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
43
|
+
CREATE POLICY plg_marketing_ranklayer_integrations_delete ON public.plg_marketing_ranklayer_integrations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
44
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_marketing_ranklayer_integrations TO authenticated;
|
|
45
|
+
|
|
46
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log;
|
|
47
|
+
DROP POLICY IF EXISTS plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log;
|
|
48
|
+
CREATE POLICY plg_marketing_ranklayer_sync_log_select ON public.plg_marketing_ranklayer_sync_log FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
49
|
+
CREATE POLICY plg_marketing_ranklayer_sync_log_insert ON public.plg_marketing_ranklayer_sync_log FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
50
|
+
GRANT SELECT, INSERT ON public.plg_marketing_ranklayer_sync_log TO authenticated;
|