Add single-record People Data Labs enrich functions (company & person) (#21650)
Adds two single-record enrichment logic functions to the People Data
Labs app — `enrich-company` and `enrich-person` — that call PDL's
single-record Enrichment endpoints (`/company/enrich`,
`/person/enrich`). Each function declares both a workflow-action trigger
and an AI-tool trigger, so the same function is usable as a workflow
step and as an AI tool. They take a single `{ recordId,
overrideExistingValues? }` and return a single `EnrichResult`.
The new functions replace the previous `enrich-company-tool` /
`enrich-person-tool` AI-tool functions (which delegated to the bulk
endpoints), avoiding duplicate near-identical tools for the LLM. The
bulk `enrich-companies` / `enrich-people` workflow actions are
unchanged.
Implementation reuses the existing enrichment machinery: the
single-record adapters spread the existing company/person adapters and
only override `enrichBatch`, so identifier extraction, TTL guard, field
mapping (fill-only-if-empty), billing, and error backoff all carry over.
A new `post-pdl-single-enrich` util posts params directly and classifies
the response via the existing `parsePdlItem`.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21650?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -12,14 +12,21 @@ Enriches **Person** and **Company** records with [People Data Labs](https://www.
|
||||
|
||||
## Enrichment logic functions
|
||||
|
||||
`enrich-person` / `enrich-company` (bulk workflow actions, for the manual record action) plus
|
||||
`enrich-person-tool` / `enrich-company-tool` (single-record AI tools) all delegate to a shared,
|
||||
trigger-agnostic core in `src/logic-functions/handlers/`:
|
||||
`enrich-companies` / `enrich-people` (bulk workflow actions, for the manual record action) and
|
||||
`enrich-company` / `enrich-person` (single-record functions exposed **both** as a workflow action
|
||||
and as an AI tool) all delegate to a shared, trigger-agnostic core in `src/logic-functions/handlers/`:
|
||||
|
||||
- The workflow-action functions accept a **list of records** (`{ records, force? }`) and loop
|
||||
the single-record core over each, aggregating the outcome (`total` / `matched` / `notFound` /
|
||||
`skipped` / `errored`); a per-record failure is captured as `ERROR` without aborting the batch
|
||||
(`src/logic-functions/utils/run-batch-enrichment.ts`). The AI tools stay single-record.
|
||||
- The bulk workflow-action functions accept a **list of records** (`{ records, overrideExistingValues? }`),
|
||||
call the PDL **bulk** Enrichment endpoints (`/person/bulk`, `/company/enrich/bulk`), and loop the
|
||||
single-record core over each, aggregating the outcome (`total` / `matched` / `notFound` / `skipped` /
|
||||
`errored`); a per-record failure is captured as `ERROR` without aborting the batch
|
||||
(`src/logic-functions/utils/run-batch-enrichment.ts`).
|
||||
- The single-record functions accept one record (`{ recordId, overrideExistingValues? }`), call the PDL
|
||||
**single-record** Enrichment endpoints (`/person/enrich`, `/company/enrich` —
|
||||
`src/logic-functions/utils/post-pdl-single-enrich.ts`), and return a single `EnrichResult`
|
||||
(`src/logic-functions/utils/run-single-enrichment.ts`). They declare both a
|
||||
`workflowActionTriggerSettings` and a `toolTriggerSettings`, so one function is usable as a workflow
|
||||
step and as an AI tool.
|
||||
|
||||
- Read the record, guard against re-enriching within a TTL (`pdlLastEnrichedAt`), pick a
|
||||
match identifier (person: `pdlId` → LinkedIn → email → name; company: `pdlId` → domain →
|
||||
@@ -32,7 +39,8 @@ trigger-agnostic core in `src/logic-functions/handlers/`:
|
||||
(`src/logic-functions/utils/`); the option sets are the same `src/constants/*-options.ts`
|
||||
the field definitions use.
|
||||
|
||||
Run locally: `yarn twenty dev:function:exec -n enrich-person -p '{"records":[{"id":"<id>"}]}'`.
|
||||
Run locally: `yarn twenty dev:function:exec -n enrich-people -p '{"records":[{"id":"<id>"}]}'` (bulk)
|
||||
or `yarn twenty dev:function:exec -n enrich-person -p '{"recordId":"<id>"}'` (single record).
|
||||
|
||||
### Billing
|
||||
|
||||
@@ -64,8 +72,8 @@ When re-enabled, each workflow is a `MANUAL` / `BULK_RECORDS` trigger wired to a
|
||||
`LOGIC_FUNCTION` step whose `records` input is bound to the selected records
|
||||
(`{{trigger.companies}}` / `{{trigger.people}}`):
|
||||
|
||||
- **Enrich companies** — runs `enrich-company` over the selected Companies.
|
||||
- **Enrich people** — runs `enrich-person` over the selected People.
|
||||
- **Enrich companies** — runs `enrich-companies` over the selected Companies.
|
||||
- **Enrich people** — runs `enrich-people` over the selected People.
|
||||
|
||||
The intended seeding (`postInstallCore`) resolves each function's runtime id from its
|
||||
`universalIdentifier` via the metadata API, publishes the version
|
||||
@@ -165,7 +173,8 @@ fields.
|
||||
|
||||
**Orchestration** (`src/logic-functions/`)
|
||||
|
||||
1. Runs from the manual "Enrich" record action (`BULK_RECORDS`) or the single-record AI tools.
|
||||
1. Runs from the manual "Enrich" record action (`BULK_RECORDS`) or the single-record
|
||||
`enrich-company` / `enrich-person` functions (as a workflow step or an AI tool).
|
||||
2. Calls the PDL Person / Company Enrichment API with `PDL_API_KEY`, passing a `min_likelihood`
|
||||
chosen by identifier strength (2 with a strong identifier, 6 for a weaker name-based match;
|
||||
overridable per call).
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const PDL_BASE_URL = 'https://api.peopledatalabs.com/v5';
|
||||
+2
-2
@@ -6,9 +6,9 @@ export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
|
||||
export const PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS = {
|
||||
enrichPeople: '65356a82-6734-4fc9-8172-7d30ed1b7859',
|
||||
enrichPersonTool: 'c1539ca9-6f57-4036-a2a7-621ec23a66e6',
|
||||
enrichPerson: '864ad69b-ff9f-4635-9aed-16fd0c2ad210',
|
||||
enrichCompanies: 'c769fb49-d495-469f-a58f-1a69ab90ec24',
|
||||
enrichCompanyTool: '88d126e1-a8f4-49f2-883f-39a7fa69cede',
|
||||
enrichCompany: '560bbfd9-1107-4f7f-8398-ea835e7e5bbe',
|
||||
postInstall: '9de46f15-05ec-4314-84c1-b9919b545269',
|
||||
} as const;
|
||||
|
||||
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
|
||||
import { enrichCompaniesCore } from 'src/logic-functions/handlers/enrich-companies';
|
||||
import { buildEmptyToolResult } from 'src/logic-functions/utils/build-empty-tool-result';
|
||||
import { buildToolRecordIds } from 'src/logic-functions/utils/build-tool-record-ids';
|
||||
import { type EnrichToolInput } from 'src/types/enrich-tool-input';
|
||||
|
||||
const handler = (input: EnrichToolInput) => {
|
||||
const records = buildToolRecordIds(input);
|
||||
|
||||
if (records.length === 0) {
|
||||
return Promise.resolve(buildEmptyToolResult());
|
||||
}
|
||||
|
||||
return enrichCompaniesCore({
|
||||
input: { records, overrideExistingValues: input.overrideExistingValues },
|
||||
});
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier:
|
||||
PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichCompanyTool,
|
||||
name: 'enrich-company-tool',
|
||||
description:
|
||||
'Enrich one or more Company records with People Data Labs data (industry, size, funding, location, etc.) given their record ids. Provide recordId for a single record or recordIds for multiple.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'The id of a single Company record to enrich.',
|
||||
},
|
||||
recordIds: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description:
|
||||
'The ids of multiple Company records to enrich in one call.',
|
||||
},
|
||||
overrideExistingValues: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Overwrite existing field values with the enriched data instead of only filling empty fields.',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
|
||||
import { enrichCompanyCore } from 'src/logic-functions/handlers/enrich-company';
|
||||
import { type SingleEnrichInput } from 'src/types/single-enrich-input';
|
||||
|
||||
const handler = (input: SingleEnrichInput) => enrichCompanyCore({ input });
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichCompany,
|
||||
name: 'enrich-company',
|
||||
description:
|
||||
'Enrich a single Company record with People Data Labs data (industry, size, funding, location, etc.) given its record id.',
|
||||
timeoutSeconds: 60,
|
||||
handler,
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Enrich Company',
|
||||
icon: 'IconSparkles',
|
||||
inputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
label: 'Record',
|
||||
},
|
||||
overrideExistingValues: {
|
||||
type: 'boolean',
|
||||
label: 'Override Existing Values',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', label: 'Success' },
|
||||
recordId: { type: 'string', label: 'Record Id' },
|
||||
status: { type: 'string', label: 'Status' },
|
||||
updatedFields: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
label: 'Updated Fields',
|
||||
},
|
||||
message: { type: 'string', label: 'Message' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'The id of the Company record to enrich.',
|
||||
},
|
||||
overrideExistingValues: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Overwrite existing field values with the enriched data instead of only filling empty fields.',
|
||||
},
|
||||
},
|
||||
required: ['recordId'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
|
||||
import { enrichPeopleCore } from 'src/logic-functions/handlers/enrich-people';
|
||||
import { buildEmptyToolResult } from 'src/logic-functions/utils/build-empty-tool-result';
|
||||
import { buildToolRecordIds } from 'src/logic-functions/utils/build-tool-record-ids';
|
||||
import { type EnrichToolInput } from 'src/types/enrich-tool-input';
|
||||
|
||||
const handler = (input: EnrichToolInput) => {
|
||||
const records = buildToolRecordIds(input);
|
||||
|
||||
if (records.length === 0) {
|
||||
return Promise.resolve(buildEmptyToolResult());
|
||||
}
|
||||
|
||||
return enrichPeopleCore({
|
||||
input: { records, overrideExistingValues: input.overrideExistingValues },
|
||||
});
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichPersonTool,
|
||||
name: 'enrich-person-tool',
|
||||
description:
|
||||
'Enrich one or more Person records with People Data Labs data (job, location, social profiles, etc.) given their record ids. Provide recordId for a single record or recordIds for multiple.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'The id of a single Person record to enrich.',
|
||||
},
|
||||
recordIds: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'The ids of multiple Person records to enrich in one call.',
|
||||
},
|
||||
overrideExistingValues: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Overwrite existing field values with the enriched data instead of only filling empty fields.',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
|
||||
import { enrichPersonCore } from 'src/logic-functions/handlers/enrich-person';
|
||||
import { type SingleEnrichInput } from 'src/types/single-enrich-input';
|
||||
|
||||
const handler = (input: SingleEnrichInput) => enrichPersonCore({ input });
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichPerson,
|
||||
name: 'enrich-person',
|
||||
description:
|
||||
'Enrich a single Person record with People Data Labs data (job, location, social profiles, etc.) given its record id.',
|
||||
timeoutSeconds: 60,
|
||||
handler,
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Enrich Person',
|
||||
icon: 'IconSparkles',
|
||||
inputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
label: 'Record',
|
||||
},
|
||||
overrideExistingValues: {
|
||||
type: 'boolean',
|
||||
label: 'Override Existing Values',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', label: 'Success' },
|
||||
recordId: { type: 'string', label: 'Record Id' },
|
||||
status: { type: 'string', label: 'Status' },
|
||||
updatedFields: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
label: 'Updated Fields',
|
||||
},
|
||||
message: { type: 'string', label: 'Message' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'The id of the Person record to enrich.',
|
||||
},
|
||||
overrideExistingValues: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Overwrite existing field values with the enriched data instead of only filling empty fields.',
|
||||
},
|
||||
},
|
||||
required: ['recordId'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { type CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { COMPANY_NODE_MOCK } from 'src/logic-functions/__mocks__/company-node.mock';
|
||||
import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock';
|
||||
import { enrichCompanyCore } from 'src/logic-functions/handlers/enrich-company';
|
||||
import { enrichCompany } from 'src/logic-functions/utils/enrich-company';
|
||||
import { type CompanyNode } from 'src/types/company-node';
|
||||
|
||||
vi.mock('src/logic-functions/utils/enrich-company', () => ({
|
||||
enrichCompany: vi.fn(),
|
||||
}));
|
||||
|
||||
const enrichCompanyMock = vi.mocked(enrichCompany);
|
||||
|
||||
type Captured = {
|
||||
updateCompany?: Record<string, unknown>;
|
||||
updateCompanies?: { filter: unknown; data: Record<string, unknown> };
|
||||
};
|
||||
|
||||
type MutationRequest = {
|
||||
updateCompany?: { __args: { id: string; data: Record<string, unknown> } };
|
||||
updateCompanies?: { __args: { filter: unknown; data: Record<string, unknown> } };
|
||||
};
|
||||
|
||||
const captureMutation = (captured: Captured) => (request: unknown) => {
|
||||
const mutation = request as MutationRequest;
|
||||
if (mutation.updateCompany) {
|
||||
captured.updateCompany = mutation.updateCompany.__args.data;
|
||||
}
|
||||
if (mutation.updateCompanies) {
|
||||
captured.updateCompanies = {
|
||||
filter: mutation.updateCompanies.__args.filter,
|
||||
data: mutation.updateCompanies.__args.data,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const buildClient = (companies: CompanyNode[], captured: Captured): CoreApiClient =>
|
||||
createCoreApiClientMock({
|
||||
queryResult: { companies: { edges: companies.map((node) => ({ node })) } },
|
||||
onMutation: captureMutation(captured),
|
||||
});
|
||||
|
||||
const runOne = (client: CoreApiClient, recordId = 'c1') =>
|
||||
enrichCompanyCore({ input: { recordId }, client });
|
||||
|
||||
beforeEach(() => {
|
||||
enrichCompanyMock.mockReset();
|
||||
});
|
||||
|
||||
describe('enrichCompanyCore', () => {
|
||||
it('fills empty standard fields and writes pdl metadata via updateCompany on a match', async () => {
|
||||
enrichCompanyMock.mockResolvedValue([
|
||||
{
|
||||
outcome: 'matched',
|
||||
httpStatus: 200,
|
||||
data: {
|
||||
id: 'pdlc',
|
||||
display_name: 'Acme Corp',
|
||||
website: 'newsite.com',
|
||||
industry: 'accounting',
|
||||
},
|
||||
},
|
||||
]);
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([COMPANY_NODE_MOCK], captured);
|
||||
|
||||
const result = await runOne(client);
|
||||
|
||||
expect(enrichCompanyMock).toHaveBeenCalledTimes(1);
|
||||
expect(result.status).toBe('MATCHED');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.recordId).toBe('c1');
|
||||
expect(result.updatedFields).toContain('name');
|
||||
expect(captured.updateCompany?.name).toBe('Acme Corp');
|
||||
expect(captured.updateCompany?.pdlIndustry).toBe('ACCOUNTING');
|
||||
expect(captured.updateCompany?.pdlEnrichmentStatus).toBe('MATCHED');
|
||||
});
|
||||
|
||||
it('records NOT_FOUND and writes the status via updateCompanies', async () => {
|
||||
enrichCompanyMock.mockResolvedValue([{ outcome: 'not_found', httpStatus: 404 }]);
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([COMPANY_NODE_MOCK], captured);
|
||||
|
||||
const result = await runOne(client);
|
||||
|
||||
expect(result.status).toBe('NOT_FOUND');
|
||||
expect(result.success).toBe(true);
|
||||
expect(captured.updateCompany).toBeUndefined();
|
||||
expect(captured.updateCompanies?.data.pdlEnrichmentStatus).toBe('NOT_FOUND');
|
||||
expect(captured.updateCompanies?.filter).toEqual({ id: { in: ['c1'] } });
|
||||
});
|
||||
|
||||
it('records ERROR and reports failure on a PDL error', async () => {
|
||||
enrichCompanyMock.mockResolvedValue([
|
||||
{ outcome: 'error', httpStatus: 500, message: 'boom' },
|
||||
]);
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([COMPANY_NODE_MOCK], captured);
|
||||
|
||||
const result = await runOne(client);
|
||||
|
||||
expect(result.status).toBe('ERROR');
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('boom');
|
||||
expect(captured.updateCompanies?.data).toEqual({
|
||||
pdlEnrichmentStatus: 'ERROR',
|
||||
pdlLastEnrichedAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('skips when there is no usable identifier', async () => {
|
||||
const captured: Captured = {};
|
||||
const client = buildClient(
|
||||
[{ ...COMPANY_NODE_MOCK, domainName: null, name: '' }],
|
||||
captured,
|
||||
);
|
||||
|
||||
const result = await runOne(client);
|
||||
|
||||
expect(result.status).toBe('SKIPPED');
|
||||
expect(enrichCompanyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('marks a missing record as ERROR', async () => {
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([], captured);
|
||||
|
||||
const result = await runOne(client, 'missing');
|
||||
|
||||
expect(result.status).toBe('ERROR');
|
||||
expect(result.error).toBe('Company missing not found');
|
||||
expect(enrichCompanyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns an ERROR without touching PDL when no record id is provided', async () => {
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([COMPANY_NODE_MOCK], captured);
|
||||
|
||||
const result = await enrichCompanyCore({ input: {}, client });
|
||||
|
||||
expect(result.status).toBe('ERROR');
|
||||
expect(result.error).toBe('No record id was provided to enrich.');
|
||||
expect(enrichCompanyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { type CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock';
|
||||
import { PERSON_NODE_MOCK } from 'src/logic-functions/__mocks__/person-node.mock';
|
||||
import { enrichPersonCore } from 'src/logic-functions/handlers/enrich-person';
|
||||
import { enrichPerson } from 'src/logic-functions/utils/enrich-person';
|
||||
import { type PersonNode } from 'src/types/person-node';
|
||||
|
||||
vi.mock('src/logic-functions/utils/enrich-person', () => ({
|
||||
enrichPerson: vi.fn(),
|
||||
}));
|
||||
|
||||
const enrichPersonMock = vi.mocked(enrichPerson);
|
||||
|
||||
type Captured = {
|
||||
updatePerson?: Record<string, unknown>;
|
||||
updatePeople?: { filter: unknown; data: Record<string, unknown> };
|
||||
};
|
||||
|
||||
type MutationRequest = {
|
||||
updatePerson?: { __args: { id: string; data: Record<string, unknown> } };
|
||||
updatePeople?: { __args: { filter: unknown; data: Record<string, unknown> } };
|
||||
};
|
||||
|
||||
const captureMutation = (captured: Captured) => (request: unknown) => {
|
||||
const mutation = request as MutationRequest;
|
||||
if (mutation.updatePerson) {
|
||||
captured.updatePerson = mutation.updatePerson.__args.data;
|
||||
}
|
||||
if (mutation.updatePeople) {
|
||||
captured.updatePeople = {
|
||||
filter: mutation.updatePeople.__args.filter,
|
||||
data: mutation.updatePeople.__args.data,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const buildClient = (people: PersonNode[], captured: Captured): CoreApiClient =>
|
||||
createCoreApiClientMock({
|
||||
queryResult: { people: { edges: people.map((node) => ({ node })) } },
|
||||
onMutation: captureMutation(captured),
|
||||
});
|
||||
|
||||
const runOne = (client: CoreApiClient, recordId = 'p1') =>
|
||||
enrichPersonCore({ input: { recordId }, client });
|
||||
|
||||
beforeEach(() => {
|
||||
enrichPersonMock.mockReset();
|
||||
});
|
||||
|
||||
describe('enrichPersonCore', () => {
|
||||
it('fills empty standard fields and writes pdl metadata via updatePerson on a match', async () => {
|
||||
enrichPersonMock.mockResolvedValue([
|
||||
{
|
||||
outcome: 'matched',
|
||||
httpStatus: 200,
|
||||
likelihood: 8,
|
||||
data: {
|
||||
id: 'pdl1',
|
||||
first_name: 'Jane',
|
||||
last_name: 'Doe',
|
||||
job_title: 'CEO',
|
||||
},
|
||||
},
|
||||
]);
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([PERSON_NODE_MOCK], captured);
|
||||
|
||||
const result = await runOne(client);
|
||||
|
||||
expect(enrichPersonMock).toHaveBeenCalledTimes(1);
|
||||
expect(result.status).toBe('MATCHED');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.recordId).toBe('p1');
|
||||
expect(captured.updatePerson?.name).toEqual({
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
});
|
||||
expect(captured.updatePerson?.jobTitle).toBe('CEO');
|
||||
expect(captured.updatePerson?.pdlEnrichmentStatus).toBe('MATCHED');
|
||||
expect(captured.updatePerson?.pdlLikelihood).toBe(8);
|
||||
});
|
||||
|
||||
it('records NOT_FOUND and writes the status via updatePeople', async () => {
|
||||
enrichPersonMock.mockResolvedValue([{ outcome: 'not_found', httpStatus: 404 }]);
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([PERSON_NODE_MOCK], captured);
|
||||
|
||||
const result = await runOne(client);
|
||||
|
||||
expect(result.status).toBe('NOT_FOUND');
|
||||
expect(captured.updatePerson).toBeUndefined();
|
||||
expect(captured.updatePeople?.data.pdlEnrichmentStatus).toBe('NOT_FOUND');
|
||||
expect(captured.updatePeople?.filter).toEqual({ id: { in: ['p1'] } });
|
||||
});
|
||||
|
||||
it('records ERROR and reports failure on a PDL error', async () => {
|
||||
enrichPersonMock.mockResolvedValue([
|
||||
{ outcome: 'error', httpStatus: 500, message: 'boom' },
|
||||
]);
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([PERSON_NODE_MOCK], captured);
|
||||
|
||||
const result = await runOne(client);
|
||||
|
||||
expect(result.status).toBe('ERROR');
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('boom');
|
||||
expect(captured.updatePeople?.data).toEqual({
|
||||
pdlEnrichmentStatus: 'ERROR',
|
||||
pdlLastEnrichedAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('overwrites a populated standard field when overrideExistingValues is set', async () => {
|
||||
enrichPersonMock.mockResolvedValue([
|
||||
{
|
||||
outcome: 'matched',
|
||||
httpStatus: 200,
|
||||
likelihood: 5,
|
||||
data: { id: 'pdl1', job_title: 'CEO' },
|
||||
},
|
||||
]);
|
||||
const captured: Captured = {};
|
||||
const client = buildClient(
|
||||
[{ ...PERSON_NODE_MOCK, jobTitle: 'Existing Title' }],
|
||||
captured,
|
||||
);
|
||||
|
||||
await enrichPersonCore({
|
||||
input: { recordId: 'p1', overrideExistingValues: true },
|
||||
client,
|
||||
});
|
||||
|
||||
expect(captured.updatePerson?.jobTitle).toBe('CEO');
|
||||
});
|
||||
|
||||
it('skips when there is no usable identifier', async () => {
|
||||
const captured: Captured = {};
|
||||
const client = buildClient(
|
||||
[{ ...PERSON_NODE_MOCK, linkedinLink: null }],
|
||||
captured,
|
||||
);
|
||||
|
||||
const result = await runOne(client);
|
||||
|
||||
expect(result.status).toBe('SKIPPED');
|
||||
expect(enrichPersonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('marks a missing record as ERROR', async () => {
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([], captured);
|
||||
|
||||
const result = await runOne(client, 'missing');
|
||||
|
||||
expect(result.status).toBe('ERROR');
|
||||
expect(result.error).toBe('Person missing not found');
|
||||
expect(enrichPersonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns an ERROR without touching PDL when no record id is provided', async () => {
|
||||
const captured: Captured = {};
|
||||
const client = buildClient([PERSON_NODE_MOCK], captured);
|
||||
|
||||
const result = await enrichPersonCore({ input: {}, client });
|
||||
|
||||
expect(result.status).toBe('ERROR');
|
||||
expect(result.error).toBe('No record id was provided to enrich.');
|
||||
expect(enrichPersonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { companyEnrichmentAdapter } from 'src/logic-functions/handlers/company-enrichment-adapter';
|
||||
import { enrichCompany } from 'src/logic-functions/utils/enrich-company';
|
||||
import { type BatchEnrichmentAdapter } from 'src/types/batch-enrichment-adapter';
|
||||
import { type CompanyNode } from 'src/types/company-node';
|
||||
import { type PdlCompanyData } from 'src/types/pdl-company-data';
|
||||
import { type PdlCompanyEnrichParams } from 'src/types/pdl-company-enrich-params';
|
||||
|
||||
export const companySingleEnrichmentAdapter: BatchEnrichmentAdapter<
|
||||
CompanyNode,
|
||||
PdlCompanyData,
|
||||
PdlCompanyEnrichParams
|
||||
> = {
|
||||
...companyEnrichmentAdapter,
|
||||
enrichBatch: enrichCompany,
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { companySingleEnrichmentAdapter } from 'src/logic-functions/handlers/company-single-enrichment-adapter';
|
||||
import { runSingleEnrichment } from 'src/logic-functions/utils/run-single-enrichment';
|
||||
import { type EnrichResult } from 'src/types/enrich-result';
|
||||
import { type SingleEnrichInput } from 'src/types/single-enrich-input';
|
||||
|
||||
export const enrichCompanyCore = ({
|
||||
input,
|
||||
client = new CoreApiClient(),
|
||||
}: {
|
||||
input: SingleEnrichInput;
|
||||
client?: CoreApiClient;
|
||||
}): Promise<EnrichResult> =>
|
||||
runSingleEnrichment({
|
||||
client,
|
||||
input,
|
||||
adapter: companySingleEnrichmentAdapter,
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { personSingleEnrichmentAdapter } from 'src/logic-functions/handlers/person-single-enrichment-adapter';
|
||||
import { runSingleEnrichment } from 'src/logic-functions/utils/run-single-enrichment';
|
||||
import { type EnrichResult } from 'src/types/enrich-result';
|
||||
import { type SingleEnrichInput } from 'src/types/single-enrich-input';
|
||||
|
||||
export const enrichPersonCore = ({
|
||||
input,
|
||||
client = new CoreApiClient(),
|
||||
}: {
|
||||
input: SingleEnrichInput;
|
||||
client?: CoreApiClient;
|
||||
}): Promise<EnrichResult> =>
|
||||
runSingleEnrichment({
|
||||
client,
|
||||
input,
|
||||
adapter: personSingleEnrichmentAdapter,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { personEnrichmentAdapter } from 'src/logic-functions/handlers/person-enrichment-adapter';
|
||||
import { enrichPerson } from 'src/logic-functions/utils/enrich-person';
|
||||
import { type BatchEnrichmentAdapter } from 'src/types/batch-enrichment-adapter';
|
||||
import { type PdlPersonData } from 'src/types/pdl-person-data';
|
||||
import { type PdlPersonEnrichParams } from 'src/types/pdl-person-enrich-params';
|
||||
import { type PersonNode } from 'src/types/person-node';
|
||||
|
||||
export const personSingleEnrichmentAdapter: BatchEnrichmentAdapter<
|
||||
PersonNode,
|
||||
PdlPersonData,
|
||||
PdlPersonEnrichParams
|
||||
> = {
|
||||
...personEnrichmentAdapter,
|
||||
enrichBatch: enrichPerson,
|
||||
};
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildToolRecordIds } from 'src/logic-functions/utils/build-tool-record-ids';
|
||||
|
||||
describe('buildToolRecordIds', () => {
|
||||
it('accepts a single recordId', () => {
|
||||
expect(buildToolRecordIds({ recordId: 'a' })).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('accepts multiple recordIds', () => {
|
||||
expect(buildToolRecordIds({ recordIds: ['a', 'b'] })).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('merges recordId and recordIds, deduping overlaps', () => {
|
||||
expect(
|
||||
buildToolRecordIds({ recordId: 'a', recordIds: ['b', 'a'] }),
|
||||
).toEqual(['b', 'a']);
|
||||
});
|
||||
|
||||
it('returns an empty array when neither is provided', () => {
|
||||
expect(buildToolRecordIds({})).toEqual([]);
|
||||
});
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { enrichCompany } from 'src/logic-functions/utils/enrich-company';
|
||||
import { postPdlSingleEnrich } from 'src/logic-functions/utils/post-pdl-single-enrich';
|
||||
|
||||
vi.mock('src/logic-functions/utils/post-pdl-single-enrich', () => ({
|
||||
postPdlSingleEnrich: vi.fn(() =>
|
||||
Promise.resolve({ outcome: 'not_found', httpStatus: 404 }),
|
||||
),
|
||||
}));
|
||||
|
||||
const postPdlSingleEnrichMock = vi.mocked(postPdlSingleEnrich);
|
||||
|
||||
describe('enrichCompany', () => {
|
||||
beforeEach(() => {
|
||||
postPdlSingleEnrichMock.mockClear();
|
||||
});
|
||||
|
||||
it('posts each record to the single company enrich endpoint with only the provided params', async () => {
|
||||
await enrichCompany([{ website: 'acme.com' }, { pdlId: 'abc' }]);
|
||||
|
||||
expect(postPdlSingleEnrichMock).toHaveBeenCalledTimes(2);
|
||||
expect(postPdlSingleEnrichMock).toHaveBeenNthCalledWith(1, {
|
||||
path: '/company/enrich',
|
||||
params: { website: 'acme.com' },
|
||||
});
|
||||
expect(postPdlSingleEnrichMock).toHaveBeenNthCalledWith(2, {
|
||||
path: '/company/enrich',
|
||||
params: { pdl_id: 'abc' },
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards min_likelihood when provided', async () => {
|
||||
await enrichCompany([{ name: 'Acme', minLikelihood: 2 }]);
|
||||
|
||||
expect(postPdlSingleEnrichMock).toHaveBeenCalledWith({
|
||||
path: '/company/enrich',
|
||||
params: { name: 'Acme', min_likelihood: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns one outcome per request, preserving order', async () => {
|
||||
postPdlSingleEnrichMock
|
||||
.mockResolvedValueOnce({ outcome: 'matched', httpStatus: 200, data: { id: '1' } })
|
||||
.mockResolvedValueOnce({ outcome: 'not_found', httpStatus: 404 });
|
||||
|
||||
const results = await enrichCompany([{ website: 'a.com' }, { website: 'b.com' }]);
|
||||
|
||||
expect(results).toEqual([
|
||||
{ outcome: 'matched', httpStatus: 200, data: { id: '1' } },
|
||||
{ outcome: 'not_found', httpStatus: 404 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { enrichPerson } from 'src/logic-functions/utils/enrich-person';
|
||||
import { postPdlSingleEnrich } from 'src/logic-functions/utils/post-pdl-single-enrich';
|
||||
|
||||
vi.mock('src/logic-functions/utils/post-pdl-single-enrich', () => ({
|
||||
postPdlSingleEnrich: vi.fn(() =>
|
||||
Promise.resolve({ outcome: 'not_found', httpStatus: 404 }),
|
||||
),
|
||||
}));
|
||||
|
||||
const postPdlSingleEnrichMock = vi.mocked(postPdlSingleEnrich);
|
||||
|
||||
describe('enrichPerson', () => {
|
||||
beforeEach(() => {
|
||||
postPdlSingleEnrichMock.mockClear();
|
||||
});
|
||||
|
||||
it('posts each record to the single person enrich endpoint with only the provided params', async () => {
|
||||
await enrichPerson([
|
||||
{ email: 'a@b.com' },
|
||||
{ pdlId: 'abc', name: 'Jane Doe', company: 'Acme' },
|
||||
]);
|
||||
|
||||
expect(postPdlSingleEnrichMock).toHaveBeenCalledTimes(2);
|
||||
expect(postPdlSingleEnrichMock).toHaveBeenNthCalledWith(1, {
|
||||
path: '/person/enrich',
|
||||
params: { email: 'a@b.com' },
|
||||
});
|
||||
expect(postPdlSingleEnrichMock).toHaveBeenNthCalledWith(2, {
|
||||
path: '/person/enrich',
|
||||
params: { pdl_id: 'abc', name: 'Jane Doe', company: 'Acme' },
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards min_likelihood when provided', async () => {
|
||||
await enrichPerson([{ profile: 'linkedin.com/in/jane', minLikelihood: 6 }]);
|
||||
|
||||
expect(postPdlSingleEnrichMock).toHaveBeenCalledWith({
|
||||
path: '/person/enrich',
|
||||
params: { profile: 'linkedin.com/in/jane', min_likelihood: 6 },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns one outcome per request, preserving order', async () => {
|
||||
postPdlSingleEnrichMock
|
||||
.mockResolvedValueOnce({
|
||||
outcome: 'matched',
|
||||
httpStatus: 200,
|
||||
likelihood: 9,
|
||||
data: { id: '1' },
|
||||
})
|
||||
.mockResolvedValueOnce({ outcome: 'not_found', httpStatus: 404 });
|
||||
|
||||
const results = await enrichPerson([{ email: 'a@b.com' }, { email: 'b@b.com' }]);
|
||||
|
||||
expect(results).toEqual([
|
||||
{ outcome: 'matched', httpStatus: 200, likelihood: 9, data: { id: '1' } },
|
||||
{ outcome: 'not_found', httpStatus: 404 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { postPdlSingleEnrich } from 'src/logic-functions/utils/post-pdl-single-enrich';
|
||||
|
||||
type FetchResponse = {
|
||||
status: number;
|
||||
ok: boolean;
|
||||
json: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
const buildResponse = (
|
||||
status: number,
|
||||
json: unknown,
|
||||
jsonThrows = false,
|
||||
): FetchResponse => ({
|
||||
status,
|
||||
ok: status >= 200 && status < 300,
|
||||
json: () =>
|
||||
jsonThrows ? Promise.reject(new Error('invalid json')) : Promise.resolve(json),
|
||||
});
|
||||
|
||||
const stubFetch = (response: FetchResponse | Error) => {
|
||||
const fetchMock = vi.fn((_url: string, _init?: RequestInit) =>
|
||||
response instanceof Error
|
||||
? Promise.reject(response)
|
||||
: Promise.resolve(response),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
return fetchMock;
|
||||
};
|
||||
|
||||
describe('postPdlSingleEnrich', () => {
|
||||
beforeEach(() => {
|
||||
process.env.PDL_API_KEY = 'secret-key';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
delete process.env.PDL_API_KEY;
|
||||
});
|
||||
|
||||
it('posts the params directly in the body (not wrapped under requests)', async () => {
|
||||
const fetchMock = stubFetch(buildResponse(200, { status: 200, data: {} }));
|
||||
|
||||
await postPdlSingleEnrich({
|
||||
path: '/person/enrich',
|
||||
params: { email: 'a@b.com' },
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toBe('https://api.peopledatalabs.com/v5/person/enrich');
|
||||
expect(JSON.parse(init.body as string)).toEqual({ email: 'a@b.com' });
|
||||
});
|
||||
|
||||
it('maps a 200 person match with a data envelope', async () => {
|
||||
stubFetch(
|
||||
buildResponse(200, { status: 200, likelihood: 8, data: { id: 'a' } }),
|
||||
);
|
||||
|
||||
const result = await postPdlSingleEnrich({
|
||||
path: '/person/enrich',
|
||||
params: { email: 'a@b.com' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
outcome: 'matched',
|
||||
httpStatus: 200,
|
||||
likelihood: 8,
|
||||
data: { id: 'a' },
|
||||
});
|
||||
});
|
||||
|
||||
it('maps a 200 company match whose fields are at the top level', async () => {
|
||||
stubFetch(
|
||||
buildResponse(200, { status: 200, likelihood: 6, id: 'c1', name: 'Acme' }),
|
||||
);
|
||||
|
||||
const result = await postPdlSingleEnrich({
|
||||
path: '/company/enrich',
|
||||
params: { name: 'Acme' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
outcome: 'matched',
|
||||
httpStatus: 200,
|
||||
likelihood: 6,
|
||||
data: { id: 'c1', name: 'Acme' },
|
||||
});
|
||||
});
|
||||
|
||||
it('treats a 404 no-match as not_found from the body status', async () => {
|
||||
stubFetch(buildResponse(404, { status: 404, error: { message: 'no match' } }));
|
||||
|
||||
const result = await postPdlSingleEnrich({
|
||||
path: '/person/enrich',
|
||||
params: { email: 'a@b.com' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ outcome: 'not_found', httpStatus: 404 });
|
||||
});
|
||||
|
||||
it('falls back to the HTTP status when the body omits status', async () => {
|
||||
stubFetch(buildResponse(404, { error: { message: 'no match' } }));
|
||||
|
||||
const result = await postPdlSingleEnrich({
|
||||
path: '/person/enrich',
|
||||
params: { email: 'a@b.com' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ outcome: 'not_found', httpStatus: 404 });
|
||||
});
|
||||
|
||||
it('reports an error on a non-2xx response with an error body', async () => {
|
||||
stubFetch(buildResponse(401, { status: 401, error: { message: 'unauthorized' } }));
|
||||
|
||||
const result = await postPdlSingleEnrich({
|
||||
path: '/person/enrich',
|
||||
params: { email: 'a@b.com' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
outcome: 'error',
|
||||
httpStatus: 401,
|
||||
message: 'unauthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('reports an error when fetch throws', async () => {
|
||||
stubFetch(new Error('network down'));
|
||||
|
||||
const result = await postPdlSingleEnrich({
|
||||
path: '/person/enrich',
|
||||
params: { email: 'a@b.com' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
outcome: 'error',
|
||||
httpStatus: 0,
|
||||
message: 'PDL request failed: network down',
|
||||
});
|
||||
});
|
||||
|
||||
it('reports an error on a non-JSON response', async () => {
|
||||
stubFetch(buildResponse(200, null, true));
|
||||
|
||||
const result = await postPdlSingleEnrich({
|
||||
path: '/person/enrich',
|
||||
params: { email: 'a@b.com' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
outcome: 'error',
|
||||
httpStatus: 200,
|
||||
message: 'PDL returned a non-JSON response (HTTP 200).',
|
||||
});
|
||||
});
|
||||
|
||||
it('drops a match below the requested min_likelihood', async () => {
|
||||
stubFetch(
|
||||
buildResponse(200, { status: 200, likelihood: 3, data: { id: 'a' } }),
|
||||
);
|
||||
|
||||
const result = await postPdlSingleEnrich({
|
||||
path: '/person/enrich',
|
||||
params: { email: 'a@b.com', min_likelihood: 6 },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ outcome: 'not_found', httpStatus: 200 });
|
||||
});
|
||||
});
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { type BulkEnrichResult } from 'src/types/bulk-enrich-result';
|
||||
|
||||
export const buildEmptyToolResult = (): BulkEnrichResult => ({
|
||||
success: false,
|
||||
total: 0,
|
||||
matched: 0,
|
||||
notFound: 0,
|
||||
skipped: 0,
|
||||
errored: 0,
|
||||
results: [],
|
||||
});
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { type EnrichToolInput } from 'src/types/enrich-tool-input';
|
||||
|
||||
export const buildToolRecordIds = (input: EnrichToolInput): string[] => {
|
||||
const nonEmptyRecordIds = [
|
||||
...(input.recordIds ?? []),
|
||||
...(input.recordId !== undefined ? [input.recordId] : []),
|
||||
].filter(isNonEmptyString);
|
||||
|
||||
return Array.from(new Set(nonEmptyRecordIds));
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { postPdlSingleEnrich } from 'src/logic-functions/utils/post-pdl-single-enrich';
|
||||
import { type PdlCompanyData } from 'src/types/pdl-company-data';
|
||||
import { type PdlCompanyEnrichParams } from 'src/types/pdl-company-enrich-params';
|
||||
import { type PdlEnrichResult } from 'src/types/pdl-enrich-result';
|
||||
import { pruneUndefined } from 'src/utils/prune-undefined';
|
||||
|
||||
export const enrichCompany = (
|
||||
params: PdlCompanyEnrichParams[],
|
||||
): Promise<PdlEnrichResult<PdlCompanyData>[]> =>
|
||||
Promise.all(
|
||||
params.map((entry) =>
|
||||
postPdlSingleEnrich<PdlCompanyData>({
|
||||
path: '/company/enrich',
|
||||
params: pruneUndefined({
|
||||
pdl_id: entry.pdlId,
|
||||
website: entry.website,
|
||||
profile: entry.profile,
|
||||
name: entry.name,
|
||||
min_likelihood: entry.minLikelihood,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { postPdlSingleEnrich } from 'src/logic-functions/utils/post-pdl-single-enrich';
|
||||
import { type PdlEnrichResult } from 'src/types/pdl-enrich-result';
|
||||
import { type PdlPersonData } from 'src/types/pdl-person-data';
|
||||
import { type PdlPersonEnrichParams } from 'src/types/pdl-person-enrich-params';
|
||||
import { pruneUndefined } from 'src/utils/prune-undefined';
|
||||
|
||||
export const enrichPerson = (
|
||||
params: PdlPersonEnrichParams[],
|
||||
): Promise<PdlEnrichResult<PdlPersonData>[]> =>
|
||||
Promise.all(
|
||||
params.map((entry) =>
|
||||
postPdlSingleEnrich<PdlPersonData>({
|
||||
path: '/person/enrich',
|
||||
params: pruneUndefined({
|
||||
pdl_id: entry.pdlId,
|
||||
profile: entry.profile,
|
||||
email: entry.email,
|
||||
name: entry.name,
|
||||
company: entry.company,
|
||||
min_likelihood: entry.minLikelihood,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
+1
-2
@@ -1,12 +1,11 @@
|
||||
import { isNumber, isObject } from '@sniptt/guards';
|
||||
|
||||
import { PDL_BASE_URL } from 'src/constants/pdl-base-url';
|
||||
import { extractPdlErrorMessage } from 'src/logic-functions/utils/extract-pdl-error-message';
|
||||
import { getPdlApiKey } from 'src/logic-functions/utils/get-pdl-api-key';
|
||||
import { parsePdlItem } from 'src/logic-functions/utils/parse-pdl-item';
|
||||
import { type PdlEnrichResult } from 'src/types/pdl-enrich-result';
|
||||
|
||||
const PDL_BASE_URL = 'https://api.peopledatalabs.com/v5';
|
||||
|
||||
export const postPdlBulkEnrich = async <TData>({
|
||||
path,
|
||||
requests,
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { isNumber, isObject } from '@sniptt/guards';
|
||||
|
||||
import { PDL_BASE_URL } from 'src/constants/pdl-base-url';
|
||||
import { getPdlApiKey } from 'src/logic-functions/utils/get-pdl-api-key';
|
||||
import { parsePdlItem } from 'src/logic-functions/utils/parse-pdl-item';
|
||||
import { type PdlEnrichResult } from 'src/types/pdl-enrich-result';
|
||||
|
||||
export const postPdlSingleEnrich = async <TData>({
|
||||
path,
|
||||
params,
|
||||
}: {
|
||||
path: string;
|
||||
params: Record<string, unknown>;
|
||||
}): Promise<PdlEnrichResult<TData>> => {
|
||||
const apiKey = getPdlApiKey();
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${PDL_BASE_URL}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': apiKey,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
return {
|
||||
outcome: 'error',
|
||||
httpStatus: 0,
|
||||
message: `PDL request failed: ${message}`,
|
||||
};
|
||||
}
|
||||
|
||||
let json: unknown;
|
||||
try {
|
||||
json = await response.json();
|
||||
} catch {
|
||||
return {
|
||||
outcome: 'error',
|
||||
httpStatus: response.status,
|
||||
message: `PDL returned a non-JSON response (HTTP ${response.status}).`,
|
||||
};
|
||||
}
|
||||
|
||||
const responseItem = isObject(json) ? (json as Record<string, unknown>) : {};
|
||||
const responseItemWithStatus = isNumber(responseItem.status)
|
||||
? responseItem
|
||||
: { ...responseItem, status: response.status };
|
||||
|
||||
return parsePdlItem<TData>({
|
||||
item: responseItemWithStatus,
|
||||
requestedMinLikelihood: isNumber(params.min_likelihood)
|
||||
? params.min_likelihood
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import {
|
||||
buildErrorResult,
|
||||
ENRICHMENT_FAILED_MESSAGE,
|
||||
} from 'src/logic-functions/utils/build-error-result';
|
||||
import { runBatchEnrichment } from 'src/logic-functions/utils/run-batch-enrichment';
|
||||
import { type BatchEnrichmentAdapter } from 'src/types/batch-enrichment-adapter';
|
||||
import { type EnrichResult } from 'src/types/enrich-result';
|
||||
import { type SingleEnrichInput } from 'src/types/single-enrich-input';
|
||||
|
||||
const NO_RECORD_ID_MESSAGE = 'No record id was provided to enrich.';
|
||||
|
||||
export const runSingleEnrichment = async <TNode, TData, TParams>({
|
||||
client,
|
||||
input,
|
||||
adapter,
|
||||
}: {
|
||||
client: CoreApiClient;
|
||||
input: SingleEnrichInput;
|
||||
adapter: BatchEnrichmentAdapter<TNode, TData, TParams>;
|
||||
}): Promise<EnrichResult> => {
|
||||
const recordId = input.recordId?.trim();
|
||||
|
||||
if (!isNonEmptyString(recordId)) {
|
||||
return buildErrorResult({ recordId: '', error: NO_RECORD_ID_MESSAGE });
|
||||
}
|
||||
|
||||
const bulkResult = await runBatchEnrichment({
|
||||
client,
|
||||
input: {
|
||||
records: recordId,
|
||||
overrideExistingValues: input.overrideExistingValues,
|
||||
minLikelihood: input.minLikelihood,
|
||||
},
|
||||
adapter,
|
||||
});
|
||||
|
||||
return (
|
||||
bulkResult.results[0] ??
|
||||
buildErrorResult({ recordId, error: ENRICHMENT_FAILED_MESSAGE })
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
export type EnrichToolInput = {
|
||||
export type SingleEnrichInput = {
|
||||
recordId?: string;
|
||||
recordIds?: string[];
|
||||
overrideExistingValues?: boolean;
|
||||
minLikelihood?: number;
|
||||
};
|
||||
Reference in New Issue
Block a user