@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.
Files changed (58) hide show
  1. package/README.md +2 -0
  2. package/dist/MarketingContext.d.ts +2 -0
  3. package/dist/MarketingContext.d.ts.map +1 -1
  4. package/dist/MarketingPage.d.ts.map +1 -1
  5. package/dist/data/blogEntities.d.ts +28 -0
  6. package/dist/data/blogEntities.d.ts.map +1 -0
  7. package/dist/data/contentSupabase.d.ts.map +1 -1
  8. package/dist/data/tables.d.ts +6 -0
  9. package/dist/data/tables.d.ts.map +1 -0
  10. package/dist/index.cjs +630 -127
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +636 -133
  14. package/dist/index.js.map +1 -1
  15. package/dist/integrations/ranklayer/connectorDef.d.ts +3 -0
  16. package/dist/integrations/ranklayer/connectorDef.d.ts.map +1 -0
  17. package/dist/integrations/ranklayer/data/supabase.d.ts +16 -0
  18. package/dist/integrations/ranklayer/data/supabase.d.ts.map +1 -0
  19. package/dist/integrations/ranklayer/types.d.ts +16 -0
  20. package/dist/integrations/ranklayer/types.d.ts.map +1 -0
  21. package/dist/migrations/index.d.ts +9 -3
  22. package/dist/migrations/index.d.ts.map +1 -1
  23. package/dist/presets.d.ts +2 -0
  24. package/dist/presets.d.ts.map +1 -1
  25. package/dist/schema/index.d.ts +44 -44
  26. package/dist/schema/index.d.ts.map +1 -1
  27. package/dist/schema.cjs +9 -9
  28. package/dist/schema.cjs.map +1 -1
  29. package/dist/schema.js +9 -9
  30. package/dist/schema.js.map +1 -1
  31. package/dist/views/blog/BlogAdminView.d.ts +5 -0
  32. package/dist/views/blog/BlogAdminView.d.ts.map +1 -0
  33. package/dist/views/blog/BlogEntityForm.d.ts +11 -0
  34. package/dist/views/blog/BlogEntityForm.d.ts.map +1 -0
  35. package/dist/views/blog/useEntityCrud.d.ts +29 -0
  36. package/dist/views/blog/useEntityCrud.d.ts.map +1 -0
  37. package/package.json +8 -5
  38. package/src/MarketingContext.tsx +2 -0
  39. package/src/MarketingPage.tsx +22 -1
  40. package/src/data/blogEntities.tsx +91 -0
  41. package/src/data/contentSupabase.ts +18 -17
  42. package/src/data/tables.ts +7 -0
  43. package/src/index.ts +27 -3
  44. package/src/integrations/ranklayer/RANKLAYER.md +40 -0
  45. package/src/integrations/ranklayer/connectorDef.tsx +98 -0
  46. package/src/integrations/ranklayer/data/supabase.ts +106 -0
  47. package/src/integrations/ranklayer/types.ts +16 -0
  48. package/src/migrations/000_plg_rename.sql +15 -0
  49. package/src/migrations/001_content_planner.sql +48 -48
  50. package/src/migrations/002_multi_platform.sql +5 -5
  51. package/src/migrations/003_recording_ops.sql +10 -10
  52. package/src/migrations/004_ranklayer.sql +50 -0
  53. package/src/migrations/index.ts +152 -74
  54. package/src/presets.ts +5 -3
  55. package/src/schema/index.ts +9 -9
  56. package/src/views/blog/BlogAdminView.tsx +99 -0
  57. package/src/views/blog/BlogEntityForm.tsx +92 -0
  58. 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: mkt_
2
+ -- Prefix: plg_marketing_
3
3
  -- Social accounts + content plans + posts (Notion-style markdown pages).
4
4
 
5
- CREATE TABLE IF NOT EXISTS public.mkt_social_accounts (
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 saas_core.tenants(id) ON DELETE CASCADE,
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.mkt_social_accounts ENABLE ROW LEVEL SECURITY;
16
- CREATE INDEX IF NOT EXISTS idx_mkt_social_accounts_tenant ON public.mkt_social_accounts(tenant_id);
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.mkt_content_plans (
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 saas_core.tenants(id) ON DELETE CASCADE,
21
- account_id uuid NOT NULL REFERENCES public.mkt_social_accounts(id) ON DELETE CASCADE,
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.mkt_content_plans ENABLE ROW LEVEL SECURITY;
37
- CREATE INDEX IF NOT EXISTS idx_mkt_content_plans_tenant ON public.mkt_content_plans(tenant_id);
38
- CREATE INDEX IF NOT EXISTS idx_mkt_content_plans_account ON public.mkt_content_plans(account_id);
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.mkt_content_posts (
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 saas_core.tenants(id) ON DELETE CASCADE,
43
- plan_id uuid NOT NULL REFERENCES public.mkt_content_plans(id) ON DELETE CASCADE,
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.mkt_content_posts ENABLE ROW LEVEL SECURITY;
59
- CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_tenant ON public.mkt_content_posts(tenant_id);
60
- CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_plan ON public.mkt_content_posts(plan_id, week_number, position);
61
- CREATE INDEX IF NOT EXISTS idx_mkt_content_posts_status ON public.mkt_content_posts(tenant_id, status);
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 mkt_social_accounts
64
- DROP POLICY IF EXISTS mkt_social_accounts_select ON public.mkt_social_accounts;
65
- DROP POLICY IF EXISTS mkt_social_accounts_insert ON public.mkt_social_accounts;
66
- DROP POLICY IF EXISTS mkt_social_accounts_update ON public.mkt_social_accounts;
67
- DROP POLICY IF EXISTS mkt_social_accounts_delete ON public.mkt_social_accounts;
68
- CREATE POLICY mkt_social_accounts_select ON public.mkt_social_accounts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
69
- CREATE POLICY mkt_social_accounts_insert ON public.mkt_social_accounts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
70
- CREATE POLICY mkt_social_accounts_update ON public.mkt_social_accounts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
71
- CREATE POLICY mkt_social_accounts_delete ON public.mkt_social_accounts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
72
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_social_accounts TO authenticated;
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 mkt_content_plans
75
- DROP POLICY IF EXISTS mkt_content_plans_select ON public.mkt_content_plans;
76
- DROP POLICY IF EXISTS mkt_content_plans_insert ON public.mkt_content_plans;
77
- DROP POLICY IF EXISTS mkt_content_plans_update ON public.mkt_content_plans;
78
- DROP POLICY IF EXISTS mkt_content_plans_delete ON public.mkt_content_plans;
79
- CREATE POLICY mkt_content_plans_select ON public.mkt_content_plans FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
80
- CREATE POLICY mkt_content_plans_insert ON public.mkt_content_plans FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
81
- CREATE POLICY mkt_content_plans_update ON public.mkt_content_plans FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
82
- CREATE POLICY mkt_content_plans_delete ON public.mkt_content_plans FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
83
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_content_plans TO authenticated;
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 mkt_content_posts
86
- DROP POLICY IF EXISTS mkt_content_posts_select ON public.mkt_content_posts;
87
- DROP POLICY IF EXISTS mkt_content_posts_insert ON public.mkt_content_posts;
88
- DROP POLICY IF EXISTS mkt_content_posts_update ON public.mkt_content_posts;
89
- DROP POLICY IF EXISTS mkt_content_posts_delete ON public.mkt_content_posts;
90
- CREATE POLICY mkt_content_posts_select ON public.mkt_content_posts FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
91
- CREATE POLICY mkt_content_posts_insert ON public.mkt_content_posts FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
92
- CREATE POLICY mkt_content_posts_update ON public.mkt_content_posts FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
93
- CREATE POLICY mkt_content_posts_delete ON public.mkt_content_posts FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
94
- GRANT SELECT, INSERT, UPDATE, DELETE ON public.mkt_content_posts TO authenticated;
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.mkt_social_accounts
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 = 'mkt_social_accounts' AND column_name = 'platform'
15
+ WHERE table_schema = 'public' AND table_name = 'plg_marketing_social_accounts' AND column_name = 'platform'
16
16
  ) THEN
17
- UPDATE public.mkt_social_accounts
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.mkt_social_accounts DROP COLUMN platform;
20
+ ALTER TABLE public.plg_marketing_social_accounts DROP COLUMN platform;
21
21
  END IF;
22
22
  END $$;
23
23
 
24
- ALTER TABLE public.mkt_content_posts
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.mkt_content_posts
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.mkt_content_posts
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 mkt_media_read ON storage.objects;
19
- DROP POLICY IF EXISTS mkt_media_insert ON storage.objects;
20
- DROP POLICY IF EXISTS mkt_media_update ON storage.objects;
21
- DROP POLICY IF EXISTS mkt_media_delete ON storage.objects;
22
- CREATE POLICY mkt_media_read ON storage.objects FOR SELECT TO public USING (bucket_id = 'mkt-media');
23
- CREATE POLICY mkt_media_insert ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'mkt-media');
24
- CREATE POLICY mkt_media_update ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'mkt-media');
25
- CREATE POLICY mkt_media_delete ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'mkt-media');
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;