Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion cms/__tests__/solutions.hook.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ describe('Solutions afterChange hook', () => {
collection: 'solutions',
status: 'success',
supabaseId: generatedId,
metadata: expect.objectContaining({ featureCount: 2, slug: 'demo-solution' }),
metadata: expect.objectContaining({
slug: 'demo-solution',
title: 'Consultoria IA',
description: 'Helping teams deliver AI responsibly.',
features: JSON.stringify(['Recurso 1', 'Second capability']),
active: true,
gradient: 'from-brand-blue to-brand-purple',
}),
})
);
});
Expand Down
136 changes: 21 additions & 115 deletions cms/src/collections/HomepageFeatures.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,38 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { CollectionConfig } from 'payload';
import { pool } from '../utilities/pool';
import { normalizeUrl } from '../utilities/validation';
import { createUpsertHook } from '../utilities/upsert-factory';
import { resolveLocalizedText } from '../utilities/localization';
import { buildSpaPreviewUrl } from '../utilities/preview';
import { logCmsSyncEvent } from '../utilities/monitoring';
import { publicReadAuthWrite, supabaseIdField, activeField } from '../utilities/collection-helpers';

const ICON_OPTIONS = ['Brain', 'Zap', 'Shield', 'Users', 'Globe', 'Laptop'];

const normalizeUrl = (value: unknown): string | null => {
if (typeof value !== 'string' || value.trim().length === 0) {
return null;
}

try {
const parsed = new URL(value.trim());
return parsed.toString();
} catch {
return null;
}
};

const upsertIntoSupabase = async ({ doc, req }: { doc: any; req: any }) => {
const supabaseId = doc.supabaseId ?? null;
const title = resolveLocalizedText(doc.title);
const description = resolveLocalizedText(doc.description);
const icon = ICON_OPTIONS.includes(doc.icon) ? doc.icon : ICON_OPTIONS[0];
const url = normalizeUrl(doc.url);
const orderIndex = Number.parseInt(String(doc.orderIndex ?? '0'), 10);
const active = !!doc.active;

const queryWithId = `
insert into public.homepage_features (id, title, description, icon, url, order_index, active, updated_at)
values ($1::uuid,$2,$3,$4,$5,$6,$7, now())
on conflict (id) do update set
title=excluded.title,
description=excluded.description,
icon=excluded.icon,
url=excluded.url,
order_index=excluded.order_index,
active=excluded.active,
updated_at=now()
returning id`;

const queryWithoutId = `
insert into public.homepage_features (title, description, icon, url, order_index, active, updated_at)
values ($1,$2,$3,$4,$5,$6, now())
on conflict (title) do update set
description=excluded.description,
icon=excluded.icon,
url=excluded.url,
order_index=excluded.order_index,
active=excluded.active,
updated_at=now()
returning id`;

const paramsWithId = [
supabaseId,
title,
description,
icon,
url,
orderIndex,
active,
];

const paramsWithoutId = [title, description, icon, url, orderIndex, active];

try {
const result = supabaseId
? await pool.query(queryWithId, paramsWithId)
: await pool.query(queryWithoutId, paramsWithoutId);

const generatedId = result?.rows?.[0]?.id ?? null;

if (!supabaseId && generatedId && req?.payload) {
await req.payload.update({
collection: 'homepageFeatures',
id: doc.id,
data: { supabaseId: generatedId },
depth: 0,
});
}

await logCmsSyncEvent({
collection: 'homepageFeatures',
action: 'upsert',
payloadId: doc.id ?? null,
supabaseId: (supabaseId ?? generatedId) ?? null,
status: 'success',
metadata: { icon, orderIndex, active, urlPresent: Boolean(url) },
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error while syncing homepage feature';
await logCmsSyncEvent({
collection: 'homepageFeatures',
action: 'upsert',
payloadId: doc.id ?? null,
supabaseId,
status: 'error',
message,
metadata: { icon, orderIndex },
});
throw error;
}
};
const upsertIntoSupabase = createUpsertHook({
collectionName: 'homepageFeatures',
tableName: 'homepage_features',
conflictFieldWithoutId: 'title',
extractData: (doc: any) => {
const title = resolveLocalizedText(doc.title);
const description = resolveLocalizedText(doc.description);
const icon = ICON_OPTIONS.includes(doc.icon) ? doc.icon : ICON_OPTIONS[0];
const url = normalizeUrl(doc.url);
const order_index = Number.parseInt(String(doc.orderIndex ?? '0'), 10);
const active = !!doc.active;

return { title, description, icon, url, order_index, active };
},
});

const HomepageFeatures: CollectionConfig = {
slug: 'homepageFeatures',
admin: {
useAsTitle: 'title',
preview: () => buildSpaPreviewUrl('/'),
},
access: {
read: () => true,
create: ({ req }: { req: any }) => Boolean(req.user),
update: ({ req }: { req: any }) => Boolean(req.user),
delete: ({ req }: { req: any }) => Boolean(req.user),
},
access: publicReadAuthWrite,
fields: [
{
name: 'supabaseId',
type: 'text',
admin: { hidden: true },
},
supabaseIdField,
{ name: 'title', type: 'text', required: true, localized: true },
{ name: 'description', type: 'textarea', required: true, localized: true },
{
Expand Down Expand Up @@ -168,11 +78,7 @@ const HomepageFeatures: CollectionConfig = {
return true;
},
},
{
name: 'active',
type: 'checkbox',
defaultValue: true,
},
activeField,
],
hooks: { afterChange: [upsertIntoSupabase] },
};
Expand Down
128 changes: 27 additions & 101 deletions cms/src/collections/Leads.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,41 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { CollectionConfig } from 'payload';
import { pool } from '../utilities/pool';
import { logCmsSyncEvent } from '../utilities/monitoring';

const normalizeEmail = (value: unknown): string | null => {
if (typeof value !== 'string') {
return null;
}

const trimmed = value.trim().toLowerCase();
if (trimmed.length === 0) {
return null;
}

const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(trimmed) ? trimmed : null;
};

const upsertIntoSupabase = async ({ doc, req }: { doc: any; req: any }) => {
const supabaseId = doc.supabaseId ?? null;
const name = typeof doc.name === 'string' ? doc.name.trim() : '';
const email = normalizeEmail(doc.email);
const company = typeof doc.company === 'string' && doc.company.trim().length > 0 ? doc.company.trim() : null;
const project = typeof doc.project === 'string' && doc.project.trim().length > 0 ? doc.project.trim() : null;
const message = typeof doc.message === 'string' ? doc.message.trim() : '';

if (!name) {
throw new Error('Name is required.');
}

if (!email) {
throw new Error('A valid email address is required.');
}

if (!message) {
throw new Error('Message cannot be empty.');
}

const queryWithId = `
insert into public.leads (id, name, email, company, project, message)
values ($1::uuid,$2,$3,$4,$5,$6)
on conflict (id) do update set
name=excluded.name,
email=excluded.email,
company=excluded.company,
project=excluded.project,
message=excluded.message
returning id`;

const queryWithoutId = `
insert into public.leads (name, email, company, project, message)
values ($1,$2,$3,$4,$5)
returning id`;

const paramsWithId = [supabaseId, name, email, company, project, message];
const paramsWithoutId = [name, email, company, project, message];

try {
const result = supabaseId
? await pool.query(queryWithId, paramsWithId)
: await pool.query(queryWithoutId, paramsWithoutId);
import { normalizeEmail } from '../utilities/validation';
import { createUpsertHook } from '../utilities/upsert-factory';
import { adminOnlyAccess, supabaseIdField } from '../utilities/collection-helpers';

const upsertIntoSupabase = createUpsertHook({
collectionName: 'leads',
tableName: 'leads',
extractData: (doc: any) => {
const name = typeof doc.name === 'string' ? doc.name.trim() : '';
const email = normalizeEmail(doc.email);
const company = typeof doc.company === 'string' && doc.company.trim().length > 0 ? doc.company.trim() : null;
const project = typeof doc.project === 'string' && doc.project.trim().length > 0 ? doc.project.trim() : null;
const message = typeof doc.message === 'string' ? doc.message.trim() : '';

if (!name) {
throw new Error('Name is required.');
}

const generatedId = result?.rows?.[0]?.id ?? null;
if (!email) {
throw new Error('A valid email address is required.');
}

if (!supabaseId && generatedId && req?.payload) {
await req.payload.update({
collection: 'leads',
id: doc.id,
data: { supabaseId: generatedId },
depth: 0,
});
if (!message) {
throw new Error('Message cannot be empty.');
}

await logCmsSyncEvent({
collection: 'leads',
action: 'upsert',
payloadId: doc.id ?? null,
supabaseId: (supabaseId ?? generatedId) ?? null,
status: 'success',
metadata: { email, hasCompany: Boolean(company), hasProject: Boolean(project) },
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error while syncing lead';
await logCmsSyncEvent({
collection: 'leads',
action: 'upsert',
payloadId: doc.id ?? null,
supabaseId,
status: 'error',
message,
metadata: { email },
});
throw error;
}
};
return { name, email, company, project, message };
},
});

const Leads: CollectionConfig = {
slug: 'leads',
admin: { useAsTitle: 'name' },
access: {
read: ({ req }: { req: any }) => Boolean(req.user),
create: ({ req }: { req: any }) => Boolean(req.user),
update: ({ req }: { req: any }) => Boolean(req.user),
delete: ({ req }: { req: any }) => Boolean(req.user),
},
access: adminOnlyAccess,
fields: [
{
name: 'supabaseId',
type: 'text',
admin: { hidden: true },
},
supabaseIdField,
{
name: 'name',
type: 'text',
Expand Down
Loading
Loading