From 37b74048108dacb1cbbc481104e8931ccd7866c2 Mon Sep 17 00:00:00 2001 From: rashad Date: Fri, 29 May 2026 05:38:55 +0400 Subject: [PATCH 1/6] feat(partners): expose rates, linkedin, picture, skills, city, country in list endpoint --- .../list-available-partners.logic-function.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts index ad0ea3f92ae11..e60cd3a56d635 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts @@ -4,6 +4,9 @@ import { defineLogicFunction } from 'twenty-sdk/define'; export const LIST_AVAILABLE_PARTNERS_LOGIC_FUNCTION_ID = '0f91164f-f492-41e8-9bb0-481be5a3d5b9'; +type CurrencyValue = { amountMicros: number; currencyCode: string } | null; +type LinkValue = { primaryLinkUrl: string | null } | null; + type Partner = { id: string; name: string | null; @@ -12,7 +15,15 @@ type Partner = { languagesSpoken: string[] | null; deploymentExpertise: string[] | null; region: string[] | null; - calendarLink: { primaryLinkUrl: string | null } | null; + calendarLink: LinkValue; + hourlyRate: CurrencyValue; + projectBudgetMin: CurrencyValue; + projectBudgetTypical: CurrencyValue; + linkedin: LinkValue; + profilePicture: LinkValue; + skills: string[] | null; + city: string | null; + country: string | null; }; type ListAvailablePartnersResult = @@ -43,6 +54,14 @@ const handler = async (): Promise => { deploymentExpertise: true, region: true, calendarLink: { primaryLinkUrl: true }, + hourlyRate: { amountMicros: true, currencyCode: true }, + projectBudgetMin: { amountMicros: true, currencyCode: true }, + projectBudgetTypical: { amountMicros: true, currencyCode: true }, + linkedin: { primaryLinkUrl: true }, + profilePicture: { primaryLinkUrl: true }, + skills: true, + city: true, + country: true, }, }, }, From a38ac86653088bc54c2f891a6571273cacba8298 Mon Sep 17 00:00:00 2001 From: rashad Date: Fri, 29 May 2026 05:40:18 +0400 Subject: [PATCH 2/6] feat(partners): add get-partner-by-slug HTTP logic function --- .../get-partner-by-slug.logic-function.ts | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts new file mode 100644 index 0000000000000..2c7f5e16e5bfc --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts @@ -0,0 +1,105 @@ +import { CoreApiClient } from 'twenty-client-sdk/core'; +import { defineLogicFunction } from 'twenty-sdk/define'; + +export const GET_PARTNER_BY_SLUG_LOGIC_FUNCTION_ID = + '5e3e7b88-2cf2-4f56-9a4a-46c4c1d6b0bb'; + +type CurrencyValue = { amountMicros: number; currencyCode: string } | null; +type LinkValue = { primaryLinkUrl: string | null } | null; + +type Partner = { + id: string; + name: string | null; + slug: string | null; + introduction: string | null; + languagesSpoken: string[] | null; + deploymentExpertise: string[] | null; + region: string[] | null; + calendarLink: LinkValue; + hourlyRate: CurrencyValue; + projectBudgetMin: CurrencyValue; + projectBudgetTypical: CurrencyValue; + linkedin: LinkValue; + profilePicture: LinkValue; + skills: string[] | null; + city: string | null; + country: string | null; +}; + +type GetPartnerBySlugResult = + | { ok: true; partner: Partner } + | { ok: false; reason: 'NOT_FOUND' | string }; + +const handler = async (input: { + slug?: string; +}): Promise => { + const slug = input?.slug; + if (typeof slug !== 'string' || slug.length === 0) { + return { ok: false, reason: 'Missing slug query parameter' }; + } + + try { + const client = new CoreApiClient(); + + const result = await client.query({ + partners: { + __args: { + filter: { + slug: { eq: slug }, + validationStage: { eq: 'VALIDATED' }, + availability: { eq: 'AVAILABLE' }, + }, + first: 1, + }, + edges: { + node: { + id: true, + name: true, + slug: true, + introduction: true, + languagesSpoken: true, + deploymentExpertise: true, + region: true, + calendarLink: { primaryLinkUrl: true }, + hourlyRate: { amountMicros: true, currencyCode: true }, + projectBudgetMin: { amountMicros: true, currencyCode: true }, + projectBudgetTypical: { amountMicros: true, currencyCode: true }, + linkedin: { primaryLinkUrl: true }, + profilePicture: { primaryLinkUrl: true }, + skills: true, + city: true, + country: true, + }, + }, + }, + } as any); + + const edges = (result?.partners?.edges ?? []) as Array<{ node: Partner }>; + const partner = edges[0]?.node; + + if (!partner) { + return { ok: false, reason: 'NOT_FOUND' }; + } + + return { ok: true, partner }; + } catch (err) { + return { + ok: false, + reason: err instanceof Error ? err.message : String(err), + }; + } +}; + +export default defineLogicFunction({ + universalIdentifier: GET_PARTNER_BY_SLUG_LOGIC_FUNCTION_ID, + name: 'get-partner-by-slug', + description: + 'Returns a single VALIDATED + AVAILABLE partner by slug, or NOT_FOUND.', + timeoutSeconds: 10, + handler, + httpRouteTriggerSettings: { + path: '/partner-by-slug', + httpMethod: 'GET', + isAuthRequired: false, + }, +}); From 0137f3c3c7237382cdd151e939db6bb60db4a3d7 Mon Sep 17 00:00:00 2001 From: rashad Date: Fri, 29 May 2026 05:46:47 +0400 Subject: [PATCH 3/6] chore(partners): bump version for new HTTP endpoint + fix slug handler input shape Read slug from queryStringParameters (LogicFunctionEvent shape) instead of top-level input; bump patch version to 0.3.2 for eventual deploy. --- packages/twenty-apps/internal/twenty-partners/package.json | 2 +- .../src/logic-functions/get-partner-by-slug.logic-function.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/twenty-apps/internal/twenty-partners/package.json b/packages/twenty-apps/internal/twenty-partners/package.json index 2b3936b4c596f..d5096b352ffe8 100644 --- a/packages/twenty-apps/internal/twenty-partners/package.json +++ b/packages/twenty-apps/internal/twenty-partners/package.json @@ -1,6 +1,6 @@ { "name": "twenty-partners", - "version": "0.3.1", + "version": "0.3.2", "license": "MIT", "engines": { "node": "^24.5.0", diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts index 2c7f5e16e5bfc..9c4ca67badc11 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts @@ -31,9 +31,9 @@ type GetPartnerBySlugResult = | { ok: false; reason: 'NOT_FOUND' | string }; const handler = async (input: { - slug?: string; + queryStringParameters?: { slug?: string }; }): Promise => { - const slug = input?.slug; + const slug = input?.queryStringParameters?.slug; if (typeof slug !== 'string' || slug.length === 0) { return { ok: false, reason: 'Missing slug query parameter' }; } From 306ad9431f74c191bd02416b62d27d91759d00f5 Mon Sep 17 00:00:00 2001 From: rashad Date: Fri, 29 May 2026 09:57:27 +0400 Subject: [PATCH 4/6] feat(partners): seed rates, linkedin, picture, skills, city for VALIDATED partners --- .../twenty-partners/src/scripts/seed.ts | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/twenty-apps/internal/twenty-partners/src/scripts/seed.ts b/packages/twenty-apps/internal/twenty-partners/src/scripts/seed.ts index 5b4bd21ea4b65..598e958f1b2bb 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/scripts/seed.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/scripts/seed.ts @@ -22,6 +22,14 @@ const requireEnv = (name: string): string => { }; const CAL = 'https://calendly.com/placeholder'; +const usd = (dollars: number) => ({ + amountMicros: dollars * 1_000_000, + currencyCode: 'USD', +}); +// Deterministic placeholder avatars; pravatar returns a stable image per `u` seed. +const avatar = (slug: string) => `https://i.pravatar.cc/300?u=${slug}`; +const linkedin = (slug: string) => + `https://www.linkedin.com/company/${slug}`; type Partner = { slug: string; @@ -37,19 +45,24 @@ type Partner = { partnerScope: string[]; typeOfTeam: string; country: string; + city: string; + hourlyRateUsd: number | null; + projectBudgetMinUsd: number | null; + projectBudgetTypicalUsd: number | null; + skills: string[]; }; const PARTNERS: Partner[] = [ - { slug: 'nine-dots-ventures', name: 'Nine Dots Ventures', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Boutique CRM implementer for real-estate workflows and WhatsApp automation.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['EUROPE', 'MENA'], languagesSpoken: ['ENGLISH', 'FRENCH', 'ARABIC'], partnerTier: 'ADVANCED', partnerScope: ['DATA_MODEL', 'WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'FRANCE' }, - { slug: 'elevate-consulting', name: 'Elevate Consulting', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Revenue-operations partner for B2B SaaS teams scaling seed to Series C.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['US', 'LATAM'], languagesSpoken: ['ENGLISH', 'SPANISH'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MIGRATION', 'DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES' }, - { slug: 'w3villa-technologies', name: 'W3Villa Technologies', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Engineering-heavy partner running large self-hosted Twenty deployments.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'MENA'], languagesSpoken: ['ENGLISH', 'HINDI'], partnerTier: 'ADVANCED', partnerScope: ['HOSTING_ENVIRONMENT', 'APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'INDIA' }, - { slug: 'act-education', name: 'Act Education', validationStage: 'VALIDATED', availability: 'UNAVAILABLE', introduction: 'CRM partner for European education providers; compliance-first self-hosting.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'NEW', partnerScope: ['HOSTING_ENVIRONMENT', 'DATA_MODEL'], typeOfTeam: 'SOLO', country: 'GERMANY' }, - { slug: 'netzero-systems', name: 'NetZero Systems', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'LATAM go-to-market partner for climate-tech and renewable-energy companies.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['LATAM', 'US'], languagesSpoken: ['ENGLISH', 'SPANISH', 'PORTUGUESE'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'BRAZIL' }, - { slug: 'meridian-craft', name: 'Meridian Craft', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'APAC implementation studio for fintech and logistics; English + Chinese.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'AFRICA'], languagesSpoken: ['ENGLISH', 'CHINESE', 'MALAY'], partnerTier: 'ADVANCED', partnerScope: ['APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'SINGAPORE' }, - { slug: 'applicant-studio', name: 'Applicant Studio', validationStage: 'APPLICATION', availability: 'UNAVAILABLE', introduction: 'New applicant; awaiting first review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'FRENCH'], partnerTier: 'NEW', partnerScope: ['DATA_MODEL'], typeOfTeam: 'SOLO', country: 'FRANCE' }, - { slug: 'rising-crm', name: 'Rising CRM', validationStage: 'POTENTIAL', availability: 'AVAILABLE', introduction: 'Promising applicant in evaluation.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['US'], languagesSpoken: ['ENGLISH'], partnerTier: 'NEW', partnerScope: ['WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES' }, - { slug: 'legacy-partners', name: 'Legacy Partners', validationStage: 'FORMER', availability: 'UNAVAILABLE', introduction: 'Former partner; no longer active in the program.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'INTERMEDIATE', partnerScope: ['HOSTING_ENVIRONMENT'], typeOfTeam: 'AGENCY', country: 'UNITED_KINGDOM' }, - { slug: 'declined-co', name: 'Declined Co', validationStage: 'REJECTED', availability: 'UNAVAILABLE', introduction: 'Application rejected after review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['MENA'], languagesSpoken: ['ENGLISH', 'ARABIC'], partnerTier: 'NEW', partnerScope: ['APPS'], typeOfTeam: 'SOLO', country: 'UNITED_ARAB_EMIRATES' }, + { slug: 'nine-dots-ventures', name: 'Nine Dots Ventures', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Boutique CRM implementer for real-estate workflows and WhatsApp automation. Nine Dots runs end-to-end Twenty rollouts for property managers and brokerages across Europe and MENA, with deep multi-language data models and AI-assisted lead intake.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['EUROPE', 'MENA'], languagesSpoken: ['ENGLISH', 'FRENCH', 'ARABIC'], partnerTier: 'ADVANCED', partnerScope: ['DATA_MODEL', 'WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'FRANCE', city: 'Paris', hourlyRateUsd: 250, projectBudgetMinUsd: 15000, projectBudgetTypicalUsd: 80000, skills: ['Real estate', 'WhatsApp', 'Multi-language', 'Workflows', 'Integrations', 'AI'] }, + { slug: 'elevate-consulting', name: 'Elevate Consulting', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Revenue-operations partner for B2B SaaS teams scaling seed to Series C. Elevate moves teams off legacy CRMs onto Twenty with a four-week migration playbook, pipeline rebuilds, and analytics handoff.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['US', 'LATAM'], languagesSpoken: ['ENGLISH', 'SPANISH'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MIGRATION', 'DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES', city: 'Austin', hourlyRateUsd: 200, projectBudgetMinUsd: 20000, projectBudgetTypicalUsd: 100000, skills: ['RevOps', 'B2B SaaS', 'Data migration', 'Pipelines', 'Salesforce migration', 'HubSpot migration'] }, + { slug: 'w3villa-technologies', name: 'W3Villa Technologies', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Engineering-heavy partner running large self-hosted Twenty deployments. Specializes in hardened Kubernetes hosting, custom integrations, and 24/7 support contracts for regulated industries across APAC and the Gulf.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'MENA'], languagesSpoken: ['ENGLISH', 'HINDI'], partnerTier: 'ADVANCED', partnerScope: ['HOSTING_ENVIRONMENT', 'APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'INDIA', city: 'Bangalore', hourlyRateUsd: 120, projectBudgetMinUsd: 10000, projectBudgetTypicalUsd: 60000, skills: ['Self-hosting', 'Kubernetes', 'DevOps', 'Integrations', 'Workflows', 'Enterprise support'] }, + { slug: 'act-education', name: 'Act Education', validationStage: 'VALIDATED', availability: 'UNAVAILABLE', introduction: 'CRM partner for European education providers; compliance-first self-hosting on EU infrastructure with full GDPR data residency and student-record workflows.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'NEW', partnerScope: ['HOSTING_ENVIRONMENT', 'DATA_MODEL'], typeOfTeam: 'SOLO', country: 'GERMANY', city: 'Berlin', hourlyRateUsd: 180, projectBudgetMinUsd: 8000, projectBudgetTypicalUsd: 40000, skills: ['Education', 'Compliance', 'Self-hosting', 'GDPR', 'Data privacy'] }, + { slug: 'netzero-systems', name: 'NetZero Systems', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'LATAM go-to-market partner for climate-tech and renewable-energy companies. Builds bilingual sales pipelines, ESG reporting, and grant-management workflows on top of Twenty.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['LATAM', 'US'], languagesSpoken: ['ENGLISH', 'SPANISH', 'PORTUGUESE'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'BRAZIL', city: 'São Paulo', hourlyRateUsd: 150, projectBudgetMinUsd: 12000, projectBudgetTypicalUsd: 50000, skills: ['Climate tech', 'Renewable energy', 'ESG reporting', 'Bilingual pipelines', 'LATAM go-to-market'] }, + { slug: 'meridian-craft', name: 'Meridian Craft', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'APAC implementation studio for fintech and logistics. Senior team of ex-bank engineers building high-throughput Twenty deployments across Singapore, Hong Kong, and Kuala Lumpur.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'AFRICA'], languagesSpoken: ['ENGLISH', 'CHINESE', 'MALAY'], partnerTier: 'ADVANCED', partnerScope: ['APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'SINGAPORE', city: 'Singapore', hourlyRateUsd: 300, projectBudgetMinUsd: 25000, projectBudgetTypicalUsd: 120000, skills: ['Fintech', 'Logistics', 'APAC', 'High throughput', 'Custom apps', 'Performance tuning'] }, + { slug: 'applicant-studio', name: 'Applicant Studio', validationStage: 'APPLICATION', availability: 'UNAVAILABLE', introduction: 'New applicant; awaiting first review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'FRENCH'], partnerTier: 'NEW', partnerScope: ['DATA_MODEL'], typeOfTeam: 'SOLO', country: 'FRANCE', city: 'Lyon', hourlyRateUsd: null, projectBudgetMinUsd: null, projectBudgetTypicalUsd: null, skills: ['Boutique', 'Design'] }, + { slug: 'rising-crm', name: 'Rising CRM', validationStage: 'POTENTIAL', availability: 'AVAILABLE', introduction: 'Promising applicant in evaluation.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['US'], languagesSpoken: ['ENGLISH'], partnerTier: 'NEW', partnerScope: ['WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES', city: 'New York', hourlyRateUsd: null, projectBudgetMinUsd: null, projectBudgetTypicalUsd: null, skills: ['SMB', 'Quick setup'] }, + { slug: 'legacy-partners', name: 'Legacy Partners', validationStage: 'FORMER', availability: 'UNAVAILABLE', introduction: 'Former partner; no longer active in the program.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'INTERMEDIATE', partnerScope: ['HOSTING_ENVIRONMENT'], typeOfTeam: 'AGENCY', country: 'UNITED_KINGDOM', city: 'London', hourlyRateUsd: null, projectBudgetMinUsd: null, projectBudgetTypicalUsd: null, skills: ['Enterprise', 'Self-hosting'] }, + { slug: 'declined-co', name: 'Declined Co', validationStage: 'REJECTED', availability: 'UNAVAILABLE', introduction: 'Application rejected after review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['MENA'], languagesSpoken: ['ENGLISH', 'ARABIC'], partnerTier: 'NEW', partnerScope: ['APPS'], typeOfTeam: 'SOLO', country: 'UNITED_ARAB_EMIRATES', city: 'Dubai', hourlyRateUsd: null, projectBudgetMinUsd: null, projectBudgetTypicalUsd: null, skills: ['MENA', 'Arabic'] }, ]; const COMPANIES = [ @@ -118,7 +131,13 @@ async function main() { introduction: p.introduction, calendarLink: { primaryLinkUrl: p.calendarLink }, deploymentExpertise: p.deploymentExpertise, region: p.region, languagesSpoken: p.languagesSpoken, partnerTier: p.partnerTier, partnerScope: p.partnerScope, typeOfTeam: p.typeOfTeam, - country: p.country, + country: p.country, city: p.city, + skills: p.skills, + linkedin: { primaryLinkUrl: linkedin(p.slug) }, + profilePicture: { primaryLinkUrl: avatar(p.slug) }, + ...(p.hourlyRateUsd != null ? { hourlyRate: usd(p.hourlyRateUsd) } : {}), + ...(p.projectBudgetMinUsd != null ? { projectBudgetMin: usd(p.projectBudgetMinUsd) } : {}), + ...(p.projectBudgetTypicalUsd != null ? { projectBudgetTypical: usd(p.projectBudgetTypicalUsd) } : {}), }; const id = partnerIdBySlug.get(p.slug); if (id) { From e3f26d7d879f6540ad68c207842ee2abf12f23b2 Mon Sep 17 00:00:00 2001 From: rashad Date: Tue, 2 Jun 2026 08:51:09 +0400 Subject: [PATCH 5/6] feat(twenty-partners): expose partnerScope on list + by-slug endpoints --- .../src/logic-functions/get-partner-by-slug.logic-function.ts | 2 ++ .../logic-functions/list-available-partners.logic-function.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts index 9c4ca67badc11..da9e63d12c652 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/get-partner-by-slug.logic-function.ts @@ -14,6 +14,7 @@ type Partner = { introduction: string | null; languagesSpoken: string[] | null; deploymentExpertise: string[] | null; + partnerScope: string[] | null; region: string[] | null; calendarLink: LinkValue; hourlyRate: CurrencyValue; @@ -59,6 +60,7 @@ const handler = async (input: { introduction: true, languagesSpoken: true, deploymentExpertise: true, + partnerScope: true, region: true, calendarLink: { primaryLinkUrl: true }, hourlyRate: { amountMicros: true, currencyCode: true }, diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts index e60cd3a56d635..aa676ba3f4d9c 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts @@ -14,6 +14,7 @@ type Partner = { introduction: string | null; languagesSpoken: string[] | null; deploymentExpertise: string[] | null; + partnerScope: string[] | null; region: string[] | null; calendarLink: LinkValue; hourlyRate: CurrencyValue; @@ -52,6 +53,7 @@ const handler = async (): Promise => { introduction: true, languagesSpoken: true, deploymentExpertise: true, + partnerScope: true, region: true, calendarLink: { primaryLinkUrl: true }, hourlyRate: { amountMicros: true, currencyCode: true }, From 89239d2c99c9e532a14d2fa8b8fa358b733e15eb Mon Sep 17 00:00:00 2001 From: rashad Date: Tue, 2 Jun 2026 12:03:52 +0400 Subject: [PATCH 6/6] refactor(twenty-partners): strictly type list-available-partners, drop `as any` The CoreApiClient is codegenerated from the synced workspace schema, so the `/partners` query is fully typed. Remove the type-lossy `as any` on the query and the `as Array<{ node: Partner }>` result cast, and derive the response type from the selection itself (dropping the hand-written Partner DTO that claimed `amountMicros: number` for a BigFloat and required fields for optional ones). This also fixes the latent "Property 'edges' does not exist on type '{}'" error the cast was masking. --- .../list-available-partners.logic-function.ts | 104 ++++++++---------- 1 file changed, 44 insertions(+), 60 deletions(-) diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts index aa676ba3f4d9c..69dd2b23cefac 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/list-available-partners.logic-function.ts @@ -4,74 +4,58 @@ import { defineLogicFunction } from 'twenty-sdk/define'; export const LIST_AVAILABLE_PARTNERS_LOGIC_FUNCTION_ID = '0f91164f-f492-41e8-9bb0-481be5a3d5b9'; -type CurrencyValue = { amountMicros: number; currencyCode: string } | null; -type LinkValue = { primaryLinkUrl: string | null } | null; +// CoreApiClient is codegenerated from the synced workspace schema, so the query +// selection is strictly typed. Keep the fetch in one place and derive the +// response shape from it, so the HTTP contract can never drift from what we +// actually ask the API for. +const queryAvailablePartners = (client: CoreApiClient) => + client.query({ + partners: { + __args: { + filter: { + validationStage: { eq: 'VALIDATED' }, + availability: { eq: 'AVAILABLE' }, + }, + orderBy: [{ name: 'AscNullsLast' }], + first: 100, + }, + edges: { + node: { + id: true, + name: true, + slug: true, + introduction: true, + languagesSpoken: true, + deploymentExpertise: true, + partnerScope: true, + region: true, + calendarLink: { primaryLinkUrl: true }, + hourlyRate: { amountMicros: true, currencyCode: true }, + projectBudgetMin: { amountMicros: true, currencyCode: true }, + projectBudgetTypical: { amountMicros: true, currencyCode: true }, + linkedin: { primaryLinkUrl: true }, + profilePicture: { primaryLinkUrl: true }, + skills: true, + city: true, + country: true, + }, + }, + }, + }); -type Partner = { - id: string; - name: string | null; - slug: string | null; - introduction: string | null; - languagesSpoken: string[] | null; - deploymentExpertise: string[] | null; - partnerScope: string[] | null; - region: string[] | null; - calendarLink: LinkValue; - hourlyRate: CurrencyValue; - projectBudgetMin: CurrencyValue; - projectBudgetTypical: CurrencyValue; - linkedin: LinkValue; - profilePicture: LinkValue; - skills: string[] | null; - city: string | null; - country: string | null; -}; +type AvailablePartner = NonNullable< + Awaited>['partners'] +>['edges'][number]['node']; type ListAvailablePartnersResult = - | { ok: true; count: number; partners: Partner[] } + | { ok: true; count: number; partners: AvailablePartner[] } | { ok: false; reason: string }; const handler = async (): Promise => { try { const client = new CoreApiClient(); - - const result = await client.query({ - partners: { - __args: { - filter: { - validationStage: { eq: 'VALIDATED' }, - availability: { eq: 'AVAILABLE' }, - }, - orderBy: [{ name: 'AscNullsLast' }], - first: 100, - }, - edges: { - node: { - id: true, - name: true, - slug: true, - introduction: true, - languagesSpoken: true, - deploymentExpertise: true, - partnerScope: true, - region: true, - calendarLink: { primaryLinkUrl: true }, - hourlyRate: { amountMicros: true, currencyCode: true }, - projectBudgetMin: { amountMicros: true, currencyCode: true }, - projectBudgetTypical: { amountMicros: true, currencyCode: true }, - linkedin: { primaryLinkUrl: true }, - profilePicture: { primaryLinkUrl: true }, - skills: true, - city: true, - country: true, - }, - }, - }, - } as any); - - const partners = ( - (result?.partners?.edges ?? []) as Array<{ node: Partner }> - ).map((edge) => edge.node); + const result = await queryAvailablePartners(client); + const partners = (result.partners?.edges ?? []).map((edge) => edge.node); return { ok: true, count: partners.length, partners }; } catch (err) {