diff --git a/packages/twenty-apps/internal/people-data-labs/.oxlintrc.json b/packages/twenty-apps/internal/people-data-labs/.oxlintrc.json index 5dc6700892038..670fc042c4e2d 100644 --- a/packages/twenty-apps/internal/people-data-labs/.oxlintrc.json +++ b/packages/twenty-apps/internal/people-data-labs/.oxlintrc.json @@ -7,10 +7,6 @@ "ignorePatterns": ["node_modules", "dist"], "rules": { "func-style": ["error", "declaration", { "allowArrowFunctions": true }], - "no-console": [ - "warn", - { "allow": ["group", "groupCollapsed", "groupEnd"] } - ], "no-control-regex": "off", "no-debugger": "error", "no-duplicate-imports": "error", diff --git a/packages/twenty-apps/internal/people-data-labs/README.md b/packages/twenty-apps/internal/people-data-labs/README.md index 520225f502ee2..6a8a764172d1e 100644 --- a/packages/twenty-apps/internal/people-data-labs/README.md +++ b/packages/twenty-apps/internal/people-data-labs/README.md @@ -2,9 +2,61 @@ Enriches **Person** and **Company** records with [People Data Labs](https://www.peopledatalabs.com/) (PDL) data. -> **Status: data-model scaffold.** This package defines the fields, relation, indexes, -> views, role, and manifest. The enrichment **logic function (the "mapper") is not yet -> implemented** — see [What the mapper must do](#what-the-mapper-must-do). +> **Status: data model + enrichment mapper.** This package defines the fields, relation, +> views, role, and manifest, and implements the enrichment **logic functions** that call the +> PDL REST API and map the response onto the standard + `pdl*` fields. The manual "Enrich" +> record-action workflows are currently **created by hand** — automatic post-install seeding is +> implemented but not wired up (see [Seeded workflows](#seeded-workflows-post-install)). + +--- + +## 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/`: + +- 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. + +- Read the record, guard against re-enriching within a TTL (`pdlLastEnrichedAt`), pick a + match identifier (person: `pdlId` → LinkedIn → email → name; company: `pdlId` → domain → + name), and call the PDL Person/Company Enrichment API (`src/logic-functions/utils/`). +- On a match: fill **standard fields only when empty** (never clobber user data), always + (re)write `pdl*` fields, and set `pdlEnrichmentStatus = MATCHED`, `pdlLastEnrichedAt`, + `pdlRawPayload` (+ `pdlLikelihood` for Person). PDL `404` → `NOT_FOUND`; other errors → + `ERROR`. No identifier / fresh TTL → skipped with no writes. +- SELECT/MULTI_SELECT values are normalized and dropped if not in the field's option set + (`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":""}]}'`. + +### Seeded workflows (post-install) + +> **Not currently wired up.** `post-install.function.ts` is a no-op +> (`return { seededWorkflows: [] }`); the seeding implementation in +> `src/logic-functions/handlers/post-install.ts` (`postInstallCore`) is **not invoked**. An +> app's `CoreApiClient` only exposes per-object CRUD over the workspace `/graphql` schema, and the +> workflow-builder mutations needed to seed a workflow (`createWorkflowVersionStep` / +> `activateWorkflowVersion`) are core resolvers the app surface does not yet expose. Until the SDK +> exposes them, **create the two "Enrich" workflows by hand**. + +When re-enabled, each workflow is a `MANUAL` / `BULK_RECORDS` trigger wired to a single +`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. + +The intended seeding (`postInstallCore`) resolves each function's runtime id from its +`universalIdentifier` via the metadata API, publishes the version +(`activateWorkflowVersion`), and is **idempotent** (skips a workflow whose name already exists). + +**Deferred to a later PR:** enrichment metering/billing, and auto-enrichment +triggers (on-create event + cron backfill). --- @@ -25,16 +77,22 @@ PDL schema v34.1**: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------ | | `pdlSeniority` (`job_title_levels`, array) | MULTI_SELECT | 10 | | `pdlFundingStages` (`funding_stages`, array) | MULTI_SELECT | 29 | -| `pdlIndustry` / `pdlJobCompanyIndustry` (`industry`) | SELECT | 147 | +| `pdlIndustry` (`industry`) | SELECT | 147 | | `pdlJobTitleSubRole` (`job_title_sub_role`) | SELECT | 106 | -| `pdlJobTitleClass`, `pdlInferredSalary`, `pdlSex`, `pdlCompanyType`, `pdlSizeRange`, `pdlJobCompanySize`, `pdlLatestFundingStage`, `pdlLocationContinent`, `pdlLocationMetro`, `pdlMicExchange` | SELECT | 5 / 11 / 2 / 6 / 8 / 8 / 29 / 7 / 384 / 70 | +| `pdlJobTitleClass`, `pdlInferredSalary`, `pdlSex`, `pdlCompanyType`, `pdlSizeRange`, `pdlLatestFundingStage`, `pdlLocationContinent`, `pdlLocationMetro`, `pdlMicExchange` | SELECT | 5 / 11 / 2 / 6 / 8 / 29 / 7 / 384 / 70 | - Option `value`s are normalized to **GraphQL enum names** (`united states` → `UNITED_STATES`): uppercase, accents stripped, non-alphanumeric → `_`, digit-leading prefixed. -- Option `universalIdentifier`s are **unique per field** (shared enums like industry get a - separate id-set per field). +- Option `universalIdentifier`s are **unique per field** (shared enums like industry, metro, and + funding stage get a separate id-set per field). +- The large option sets (`metro-options.ts`, `industry-options.ts`, …) and the UUID registry + (`universal-identifiers.ts`) are generated from the PDL taxonomy and checked in. When + regenerating for a newer PDL schema, **never change an existing option or field UUID** — that + orphans stored data; only append ids for new options. `select-option-constants.spec.ts` guards + global UUID uniqueness, value normalization, and per-field id integrity. - **Stays `TEXT`** (no canonical PDL enum file exists): `pdlIndustryDetail` (`industry_v2`), - `pdlJobOnetCode`, `pdlLocationRegion`. + `pdlJobOnetCode`. PDL `location_region` has no dedicated field — it fills the `state` slot of + the person `pdlLocation` ADDRESS composite. ### Standard-field mapping @@ -58,11 +116,19 @@ into the standard bags). - `pdlLocationMetro` (both) and `pdlLocationContinent` (company) stay SELECT — ADDRESS has no slot. _Trade-off:_ ADDRESS `country` is free text, so the country SELECT was dropped. -### Relation +### Current company → standard `company` + +PDL's detected current employer (`job_company_*`) is resolved to a Company record +(**find-or-create**, matched by `pdlId` → domain → LinkedIn → name; created with +`name` / `domainName` / `linkedinLink` + `pdlId` / `pdlIndustry` / `pdlSizeRange` when none +matches) and linked via the **standard `company`** relation, **fill-only-if-empty** — it never +overwrites a company the user already set, and the lookup is skipped entirely when the person +already has one (no orphan companies). -Dedicated **`pdlCurrentCompany`** (Person `MANY_TO_ONE` → Company) ↔ inverse -**`pdlCurrentEmployees`** (Company `ONE_TO_MANY` → Person). Deliberately **not** the standard -`company` relation, so PDL's detected employer can't overwrite the user's CRM account link. +Company attributes live on the **Company** record, not denormalized on the Person. The earlier +`pdlCurrentCompany` / `pdlCurrentEmployees` relation and the six `pdlJobCompany*` scalar fields +were **removed** as duplicates of the standard `company` relation and the linked Company's own +fields. ### Enrichment metadata @@ -75,40 +141,40 @@ Dedicated **`pdlCurrentCompany`** (Person `MANY_TO_ONE` → Company) ↔ inverse ### Other - `pdlTotalFunding` is `CURRENCY` (mapper must convert the bare USD float → micros). -- **Indexes**: `pdlId` and `pdlLastEnrichedAt` on both objects. - **Views**: a curated "People Data Labs" TABLE view per object. - **Role**: read/update on Person & Company (object-level; tighten to field-scoped later). --- -## What the mapper must do - -The logic function (to be built) must: +## What the mapper does -**Orchestration** +**Orchestration** (`src/logic-functions/`) -1. Trigger via manual command-menu action / record create / batch (TBD). -2. Call PDL Person and/or Company Enrichment with `PDL_API_KEY`; pass `min_likelihood` / - `required_fields` to control match quality. -3. On `200` → write fields + set `pdlEnrichmentStatus = MATCHED`; on `404` → - `NOT_FOUND`; on error → `ERROR`. -4. Respect PDL rate limits (queue / throttle on `429`). -5. **TTL guard**: skip re-enrichment if `pdlLastEnrichedAt` is recent; prefer re-enriching by `pdlId`. +1. Runs from the manual "Enrich" record action (`BULK_RECORDS`) or the single-record AI tools. +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). +3. A match → `pdlEnrichmentStatus = MATCHED`; PDL `404` / no match → `NOT_FOUND`; other errors → + `ERROR`. Errored and not-found records are also stamped with `pdlLastEnrichedAt` so the TTL + guard backs off instead of re-submitting them on every run. +4. **TTL guard**: skips re-enrichment when `pdlLastEnrichedAt` is within 7 days (bypass with + `force`), and prefers re-enriching by `pdlId`. **Field writing** -6. Write **standard fields** (fill-only-if-empty to avoid overwriting user data): - Person `name`, `emails`, `phones`, `linkedinLink`, `jobTitle`; Company `name`, - `domainName`, `linkedinLink`, `address`. -7. Write `pdl*` fields for everything else. -8. **SELECT guard**: only write a SELECT/MULTI_SELECT value if the normalized value exists in - the field's option set; otherwise skip and keep it in `pdlRawPayload` (handles PDL schema - versions newer than v34.1). Use the same normalization as the option `value`s. -9. **MULTI_SELECT** arrays: `job_title_levels` → `pdlSeniority`; `funding_stages` → `pdlFundingStages`. -10. **CURRENCY**: `total_funding_raised` (USD float) → `{ amountMicros: value × 1_000_000, currencyCode: 'USD' }`. -11. **ADDRESS**: split PDL `location.*` into the composite — Company → standard `address`, - Person → `pdlLocation`. -12. **Relation**: resolve `job_company_id` → find/upsert a Company record → link `pdlCurrentCompany`. -13. **Dates**: handle partial PDL dates (`YYYY`, `YYYY-MM`) for `job_start_date`, - `last_funding_date`, `birth_date`. -14. Always set `pdlId`, `pdlLastEnrichedAt`, `pdlRawPayload`, `pdlLikelihood` (person). +5. **Standard fields** are filled **only when empty** (never clobber user data): Person `name`, + `emails`, `phones`, `linkedinLink`, `jobTitle`; Company `name`, `domainName`, `linkedinLink`, + `address`. All `pdl*` fields are (re)written on every match. +6. **SELECT guard**: a SELECT/MULTI_SELECT value is written only if its normalized form is in the + field's option set; otherwise it is skipped and preserved in `pdlRawPayload` (handles PDL + schema versions newer than the bundled one). `job_title_levels` → `pdlSeniority`, + `funding_stages` → `pdlFundingStages`. +7. **CURRENCY**: `total_funding_raised` (USD) → `{ amountMicros: value × 1_000_000, currencyCode: 'USD' }`. +8. **ADDRESS**: PDL `location.*` is split into the composite — Company → standard `address`, + Person → `pdlLocation`. +9. **Current company**: `job_company_*` is resolved to a Company record (find-or-create, matched by + `pdlId` → domain → LinkedIn → name) and linked via the standard `company` relation + (fill-only-if-empty); resolutions are cached within a batch run. +10. **Dates**: partial PDL dates (`YYYY`, `YYYY-MM`) for `job_start_date`, `last_funding_date`, + `birth_date` are expanded and range-validated. +11. Always sets `pdlId`, `pdlLastEnrichedAt`, `pdlRawPayload` (+ `pdlLikelihood` for Person). diff --git a/packages/twenty-apps/internal/people-data-labs/package.json b/packages/twenty-apps/internal/people-data-labs/package.json index bda9ada3f763f..281fddd3f6490 100644 --- a/packages/twenty-apps/internal/people-data-labs/package.json +++ b/packages/twenty-apps/internal/people-data-labs/package.json @@ -16,8 +16,10 @@ "twenty": "twenty", "lint": "oxlint -c .oxlintrc.json .", "lint:fix": "oxlint --fix -c .oxlintrc.json .", - "test": "vitest run --passWithNoTests", - "test:watch": "vitest" + "typecheck": "tsc --noEmit -p tsconfig.spec.json", + "test": "vitest run --config vitest.unit.config.ts --passWithNoTests", + "test:watch": "vitest --config vitest.unit.config.ts", + "test:integration": "vitest run --passWithNoTests" }, "dependencies": { "twenty-client-sdk": "2.10.1", diff --git a/packages/twenty-apps/internal/people-data-labs/public/people-data-labs-icon.png b/packages/twenty-apps/internal/people-data-labs/public/people-data-labs-icon.png new file mode 100644 index 0000000000000..92a5d9a68fc19 Binary files /dev/null and b/packages/twenty-apps/internal/people-data-labs/public/people-data-labs-icon.png differ diff --git a/packages/twenty-apps/internal/people-data-labs/src/application-config.ts b/packages/twenty-apps/internal/people-data-labs/src/application-config.ts index cf84166cac371..9fd1b8d689df0 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/application-config.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/application-config.ts @@ -6,6 +6,7 @@ export default defineApplication({ universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, displayName: 'People Data Labs', description: 'Enrich People and Companies with People Data Labs data.', + logoUrl: 'public/people-data-labs-icon.png', serverVariables: { PDL_API_KEY: { description: 'People Data Labs API key', diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/__tests__/select-option-constants.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/__tests__/select-option-constants.spec.ts new file mode 100644 index 0000000000000..22656b67997ab --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/__tests__/select-option-constants.spec.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from 'vitest'; + +import { COMPANY_TYPE_OPTIONS } from 'src/constants/company-type-options'; +import { ENRICHMENT_STATUS_OPTIONS } from 'src/constants/enrichment-status-options'; +import { FUNDING_STAGE_OPTIONS } from 'src/constants/funding-stage-options'; +import { INDUSTRY_OPTIONS } from 'src/constants/industry-options'; +import { INFERRED_SALARY_OPTIONS } from 'src/constants/inferred-salary-options'; +import { JOB_ROLE_OPTIONS } from 'src/constants/job-role-options'; +import { JOB_TITLE_CLASS_OPTIONS } from 'src/constants/job-title-class-options'; +import { JOB_TITLE_SUB_ROLE_OPTIONS } from 'src/constants/job-title-sub-role-options'; +import { LOCATION_CONTINENT_OPTIONS } from 'src/constants/location-continent-options'; +import { METRO_OPTIONS } from 'src/constants/metro-options'; +import { MIC_EXCHANGE_OPTIONS } from 'src/constants/mic-exchange-options'; +import { SENIORITY_OPTIONS } from 'src/constants/seniority-options'; +import { SEX_OPTIONS } from 'src/constants/sex-options'; +import { SIZE_OPTIONS } from 'src/constants/size-options'; +import { + APPLICATION_UNIVERSAL_IDENTIFIER, + DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + PDL_FIELD_UNIVERSAL_IDENTIFIERS, + PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS, + PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, + PDL_VIEW_UNIVERSAL_IDENTIFIERS, +} from 'src/constants/universal-identifiers'; +import { type SelectOptionMeta } from 'src/types/select-option-meta'; +import { normalizeEnumValue } from 'src/utils/normalize-enum-value'; + +import companyEnrichmentStatusField from 'src/fields/company/pdl-enrichment-status.field'; +import companyFundingStagesField from 'src/fields/company/pdl-funding-stages.field'; +import companyIndustryField from 'src/fields/company/pdl-industry.field'; +import companyLatestFundingStageField from 'src/fields/company/pdl-latest-funding-stage.field'; +import companyLocationContinentField from 'src/fields/company/pdl-location-continent.field'; +import companyLocationMetroField from 'src/fields/company/pdl-location-metro.field'; +import companyMicExchangeField from 'src/fields/company/pdl-mic-exchange.field'; +import companySizeRangeField from 'src/fields/company/pdl-size-range.field'; +import companyTypeField from 'src/fields/company/pdl-company-type.field'; +import personEnrichmentStatusField from 'src/fields/person/pdl-enrichment-status.field'; +import personIndustryField from 'src/fields/person/pdl-industry.field'; +import personInferredSalaryField from 'src/fields/person/pdl-inferred-salary.field'; +import personJobRoleField from 'src/fields/person/pdl-job-role.field'; +import personJobTitleClassField from 'src/fields/person/pdl-job-title-class.field'; +import personJobTitleSubRoleField from 'src/fields/person/pdl-job-title-sub-role.field'; +import personLocationMetroField from 'src/fields/person/pdl-location-metro.field'; +import personSeniorityField from 'src/fields/person/pdl-seniority.field'; +import personSexField from 'src/fields/person/pdl-sex.field'; + +type FieldOption = { id?: string; value: string }; + +const fieldOptions = (field: unknown): FieldOption[] => + (field as { config?: { options?: FieldOption[] } }).config?.options ?? []; + +const selectFieldCases: { + name: string; + field: unknown; + options: readonly SelectOptionMeta[]; +}[] = [ + { name: 'person.pdlSeniority', field: personSeniorityField, options: SENIORITY_OPTIONS }, + { name: 'person.pdlJobRole', field: personJobRoleField, options: JOB_ROLE_OPTIONS }, + { name: 'person.pdlJobTitleClass', field: personJobTitleClassField, options: JOB_TITLE_CLASS_OPTIONS }, + { name: 'person.pdlJobTitleSubRole', field: personJobTitleSubRoleField, options: JOB_TITLE_SUB_ROLE_OPTIONS }, + { name: 'person.pdlSex', field: personSexField, options: SEX_OPTIONS }, + { name: 'person.pdlInferredSalary', field: personInferredSalaryField, options: INFERRED_SALARY_OPTIONS }, + { name: 'person.pdlIndustry', field: personIndustryField, options: INDUSTRY_OPTIONS }, + { name: 'person.pdlLocationMetro', field: personLocationMetroField, options: METRO_OPTIONS }, + { name: 'person.pdlEnrichmentStatus', field: personEnrichmentStatusField, options: ENRICHMENT_STATUS_OPTIONS }, + { name: 'company.pdlCompanyType', field: companyTypeField, options: COMPANY_TYPE_OPTIONS }, + { name: 'company.pdlLocationContinent', field: companyLocationContinentField, options: LOCATION_CONTINENT_OPTIONS }, + { name: 'company.pdlMicExchange', field: companyMicExchangeField, options: MIC_EXCHANGE_OPTIONS }, + { name: 'company.pdlIndustry', field: companyIndustryField, options: INDUSTRY_OPTIONS }, + { name: 'company.pdlLocationMetro', field: companyLocationMetroField, options: METRO_OPTIONS }, + { name: 'company.pdlSizeRange', field: companySizeRangeField, options: SIZE_OPTIONS }, + { name: 'company.pdlLatestFundingStage', field: companyLatestFundingStageField, options: FUNDING_STAGE_OPTIONS }, + { name: 'company.pdlFundingStages', field: companyFundingStagesField, options: FUNDING_STAGE_OPTIONS }, + { name: 'company.pdlEnrichmentStatus', field: companyEnrichmentStatusField, options: ENRICHMENT_STATUS_OPTIONS }, +]; + +describe.each(selectFieldCases)('$name options', ({ field, options }) => { + it('resolves a universalIdentifier for every option', () => { + expect(fieldOptions(field).every((option) => Boolean(option.id))).toBe(true); + }); + + it('exposes exactly the values from its source constant', () => { + expect(fieldOptions(field).map((option) => option.value)).toEqual( + options.map((option) => option.value), + ); + }); + + it('uses option values that are normalizeEnumValue fixed points', () => { + for (const option of fieldOptions(field)) { + expect(normalizeEnumValue(option.value)).toBe(option.value); + } + }); + + it('has option ids that are unique within the field', () => { + const ids = fieldOptions(field).map((option) => option.id); + expect(new Set(ids).size).toBe(ids.length); + }); +}); + +describe('select fields collectively', () => { + it('never reuses an option id across two different fields', () => { + const everyOptionId = selectFieldCases.flatMap(({ field }) => + fieldOptions(field).map((option) => option.id), + ); + + expect(new Set(everyOptionId).size).toBe(everyOptionId.length); + }); +}); + +const UUID_V4_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +const collectUuids = (value: unknown): string[] => { + if (typeof value === 'string') { + return [value]; + } + if (value !== null && typeof value === 'object') { + return Object.values(value).flatMap(collectUuids); + } + return []; +}; + +describe('universal-identifier registry', () => { + const registryUuids = collectUuids([ + APPLICATION_UNIVERSAL_IDENTIFIER, + DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, + PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS, + PDL_FIELD_UNIVERSAL_IDENTIFIERS, + PDL_VIEW_UNIVERSAL_IDENTIFIERS, + PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, + ]); + + it('contains only valid v4 UUIDs', () => { + expect(registryUuids.filter((uuid) => !UUID_V4_REGEX.test(uuid))).toEqual( + [], + ); + }); + + it('contains no duplicate UUIDs across the whole registry', () => { + expect(new Set(registryUuids).size).toBe(registryUuids.length); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/company-type-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/company-type-options.ts new file mode 100644 index 0000000000000..3b6a5a7c27689 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/company-type-options.ts @@ -0,0 +1,46 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const COMPANY_TYPE_OPTIONS: readonly SelectOptionMeta[] = [ + { + key: 'public', + value: 'PUBLIC', + label: 'Public', + color: 'blue', + position: 0, + }, + { + key: 'private', + value: 'PRIVATE', + label: 'Private', + color: 'red', + position: 1, + }, + { + key: 'publicSubsidiary', + value: 'PUBLIC_SUBSIDIARY', + label: 'Public Subsidiary', + color: 'green', + position: 2, + }, + { + key: 'educational', + value: 'EDUCATIONAL', + label: 'Educational', + color: 'orange', + position: 3, + }, + { + key: 'government', + value: 'GOVERNMENT', + label: 'Government', + color: 'purple', + position: 4, + }, + { + key: 'nonprofit', + value: 'NONPROFIT', + label: 'Nonprofit', + color: 'yellow', + position: 5, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/enrichment-status-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/enrichment-status-options.ts index 6ef98c342fff4..ec36d42f45b2c 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/constants/enrichment-status-options.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/enrichment-status-options.ts @@ -1,4 +1,4 @@ -import { type SelectOptionMeta } from 'src/types/select-option-meta.type'; +import { type SelectOptionMeta } from 'src/types/select-option-meta'; export const ENRICHMENT_STATUS_OPTIONS: readonly SelectOptionMeta[] = [ { diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/enrichment-workflow-seeds.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/enrichment-workflow-seeds.ts new file mode 100644 index 0000000000000..36f5f04e4b126 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/enrichment-workflow-seeds.ts @@ -0,0 +1,27 @@ +import { PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; +import { type EnrichmentWorkflowSeed } from 'src/types/enrichment-workflow-seed'; + +const ENRICHMENT_ICON = 'IconSparkles'; + +export const ENRICHMENT_WORKFLOW_SEEDS: EnrichmentWorkflowSeed[] = [ + { + objectNameSingular: 'company', + workflowName: 'Enrich companies with People Data Labs', + triggerName: 'When companies are selected', + icon: ENRICHMENT_ICON, + stepName: 'Enrich Companies', + logicFunctionUniversalIdentifier: + PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichCompanies, + logicFunctionInput: { records: '{{trigger.companies}}' }, + }, + { + objectNameSingular: 'person', + workflowName: 'Enrich people with People Data Labs', + triggerName: 'When people are selected', + icon: ENRICHMENT_ICON, + stepName: 'Enrich People', + logicFunctionUniversalIdentifier: + PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichPeople, + logicFunctionInput: { records: '{{trigger.people}}' }, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/funding-stage-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/funding-stage-options.ts index f41ba7b3b9cff..23b9c71955849 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/constants/funding-stage-options.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/funding-stage-options.ts @@ -1,4 +1,4 @@ -import { type SelectOptionMeta } from 'src/types/select-option-meta.type'; +import { type SelectOptionMeta } from 'src/types/select-option-meta'; export const FUNDING_STAGE_OPTIONS: readonly SelectOptionMeta[] = [ { diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/industry-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/industry-options.ts index e0d7ef5af71bc..8dfca17ba431a 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/constants/industry-options.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/industry-options.ts @@ -1,4 +1,4 @@ -import { type SelectOptionMeta } from 'src/types/select-option-meta.type'; +import { type SelectOptionMeta } from 'src/types/select-option-meta'; export const INDUSTRY_OPTIONS: readonly SelectOptionMeta[] = [ { diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/inferred-salary-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/inferred-salary-options.ts new file mode 100644 index 0000000000000..14d82b8c890dc --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/inferred-salary-options.ts @@ -0,0 +1,81 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const INFERRED_SALARY_OPTIONS: readonly SelectOptionMeta[] = [ + { + key: 'under20000', + value: 'UNDER_20000', + label: '<20K', + color: 'blue', + position: 0, + }, + { + key: 'from20000To25000', + value: 'FROM_20000_TO_25000', + label: '20K-25K', + color: 'red', + position: 1, + }, + { + key: 'from25000To35000', + value: 'FROM_25000_TO_35000', + label: '25K-35K', + color: 'green', + position: 2, + }, + { + key: 'from35000To45000', + value: 'FROM_35000_TO_45000', + label: '35K-45K', + color: 'orange', + position: 3, + }, + { + key: 'from45000To55000', + value: 'FROM_45000_TO_55000', + label: '45K-55K', + color: 'purple', + position: 4, + }, + { + key: 'from55000To70000', + value: 'FROM_55000_TO_70000', + label: '55K-70K', + color: 'yellow', + position: 5, + }, + { + key: 'from70000To85000', + value: 'FROM_70000_TO_85000', + label: '70K-85K', + color: 'pink', + position: 6, + }, + { + key: 'from85000To100000', + value: 'FROM_85000_TO_100000', + label: '85K-100K', + color: 'cyan', + position: 7, + }, + { + key: 'from100000To150000', + value: 'FROM_100000_TO_150000', + label: '100K-150K', + color: 'brown', + position: 8, + }, + { + key: 'from150000To250000', + value: 'FROM_150000_TO_250000', + label: '150K-250K', + color: 'lime', + position: 9, + }, + { + key: 'over250000', + value: 'OVER_250000', + label: '>250K', + color: 'violet', + position: 10, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/job-role-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/job-role-options.ts new file mode 100644 index 0000000000000..8c26d7bfa71d1 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/job-role-options.ts @@ -0,0 +1,172 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const JOB_ROLE_OPTIONS: readonly SelectOptionMeta[] = [ + { + key: 'advisory', + value: 'ADVISORY', + label: 'Advisory', + color: 'blue', + position: 0, + }, + { + key: 'analyst', + value: 'ANALYST', + label: 'Analyst', + color: 'red', + position: 1, + }, + { + key: 'creative', + value: 'CREATIVE', + label: 'Creative', + color: 'green', + position: 2, + }, + { + key: 'education', + value: 'EDUCATION', + label: 'Education', + color: 'orange', + position: 3, + }, + { + key: 'engineering', + value: 'ENGINEERING', + label: 'Engineering', + color: 'purple', + position: 4, + }, + { + key: 'finance', + value: 'FINANCE', + label: 'Finance', + color: 'yellow', + position: 5, + }, + { + key: 'fulfillment', + value: 'FULFILLMENT', + label: 'Fulfillment', + color: 'pink', + position: 6, + }, + { + key: 'health', + value: 'HEALTH', + label: 'Health', + color: 'cyan', + position: 7, + }, + { + key: 'hospitality', + value: 'HOSPITALITY', + label: 'Hospitality', + color: 'brown', + position: 8, + }, + { + key: 'humanResources', + value: 'HUMAN_RESOURCES', + label: 'Human Resources', + color: 'lime', + position: 9, + }, + { + key: 'legal', + value: 'LEGAL', + label: 'Legal', + color: 'violet', + position: 10, + }, + { + key: 'manufacturing', + value: 'MANUFACTURING', + label: 'Manufacturing', + color: 'gold', + position: 11, + }, + { + key: 'marketing', + value: 'MARKETING', + label: 'Marketing', + color: 'turquoise', + position: 12, + }, + { + key: 'operations', + value: 'OPERATIONS', + label: 'Operations', + color: 'crimson', + position: 13, + }, + { + key: 'partnerships', + value: 'PARTNERSHIPS', + label: 'Partnerships', + color: 'sky', + position: 14, + }, + { + key: 'product', + value: 'PRODUCT', + label: 'Product', + color: 'amber', + position: 15, + }, + { + key: 'professionalService', + value: 'PROFESSIONAL_SERVICE', + label: 'Professional Service', + color: 'plum', + position: 16, + }, + { + key: 'publicService', + value: 'PUBLIC_SERVICE', + label: 'Public Service', + color: 'grass', + position: 17, + }, + { + key: 'research', + value: 'RESEARCH', + label: 'Research', + color: 'tomato', + position: 18, + }, + { + key: 'sales', + value: 'SALES', + label: 'Sales', + color: 'iris', + position: 19, + }, + { + key: 'salesEngineering', + value: 'SALES_ENGINEERING', + label: 'Sales Engineering', + color: 'mint', + position: 20, + }, + { + key: 'support', + value: 'SUPPORT', + label: 'Support', + color: 'ruby', + position: 21, + }, + { + key: 'trade', + value: 'TRADE', + label: 'Trade', + color: 'bronze', + position: 22, + }, + { + key: 'unemployed', + value: 'UNEMPLOYED', + label: 'Unemployed', + color: 'jade', + position: 23, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/job-title-class-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/job-title-class-options.ts new file mode 100644 index 0000000000000..7d6c5c6978584 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/job-title-class-options.ts @@ -0,0 +1,39 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const JOB_TITLE_CLASS_OPTIONS: readonly SelectOptionMeta[] = [ + { + key: 'generalAndAdministrative', + value: 'GENERAL_AND_ADMINISTRATIVE', + label: 'General and Administrative', + color: 'blue', + position: 0, + }, + { + key: 'researchAndDevelopment', + value: 'RESEARCH_AND_DEVELOPMENT', + label: 'Research and Development', + color: 'red', + position: 1, + }, + { + key: 'salesAndMarketing', + value: 'SALES_AND_MARKETING', + label: 'Sales and Marketing', + color: 'green', + position: 2, + }, + { + key: 'services', + value: 'SERVICES', + label: 'Services', + color: 'orange', + position: 3, + }, + { + key: 'unemployed', + value: 'UNEMPLOYED', + label: 'Unemployed', + color: 'purple', + position: 4, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/job-title-sub-role-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/job-title-sub-role-options.ts new file mode 100644 index 0000000000000..a692fd65bd487 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/job-title-sub-role-options.ts @@ -0,0 +1,746 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const JOB_TITLE_SUB_ROLE_OPTIONS: readonly SelectOptionMeta[] = [ + { + key: 'academic', + value: 'ACADEMIC', + label: 'Academic', + color: 'blue', + position: 0, + }, + { + key: 'accountExecutive', + value: 'ACCOUNT_EXECUTIVE', + label: 'Account Executive', + color: 'red', + position: 1, + }, + { + key: 'accountManagement', + value: 'ACCOUNT_MANAGEMENT', + label: 'Account Management', + color: 'green', + position: 2, + }, + { + key: 'accounting', + value: 'ACCOUNTING', + label: 'Accounting', + color: 'orange', + position: 3, + }, + { + key: 'accountingServices', + value: 'ACCOUNTING_SERVICES', + label: 'Accounting Services', + color: 'purple', + position: 4, + }, + { + key: 'administrative', + value: 'ADMINISTRATIVE', + label: 'Administrative', + color: 'yellow', + position: 5, + }, + { + key: 'advisor', + value: 'ADVISOR', + label: 'Advisor', + color: 'pink', + position: 6, + }, + { + key: 'agriculture', + value: 'AGRICULTURE', + label: 'Agriculture', + color: 'cyan', + position: 7, + }, + { + key: 'aides', + value: 'AIDES', + label: 'Aides', + color: 'brown', + position: 8, + }, + { + key: 'architecture', + value: 'ARCHITECTURE', + label: 'Architecture', + color: 'lime', + position: 9, + }, + { + key: 'artist', + value: 'ARTIST', + label: 'Artist', + color: 'violet', + position: 10, + }, + { + key: 'boardMember', + value: 'BOARD_MEMBER', + label: 'Board Member', + color: 'gold', + position: 11, + }, + { + key: 'bookkeeping', + value: 'BOOKKEEPING', + label: 'Bookkeeping', + color: 'turquoise', + position: 12, + }, + { + key: 'brand', + value: 'BRAND', + label: 'Brand', + color: 'crimson', + position: 13, + }, + { + key: 'buildingAndGrounds', + value: 'BUILDING_AND_GROUNDS', + label: 'Building and Grounds', + color: 'sky', + position: 14, + }, + { + key: 'businessAnalyst', + value: 'BUSINESS_ANALYST', + label: 'Business Analyst', + color: 'amber', + position: 15, + }, + { + key: 'businessDevelopment', + value: 'BUSINESS_DEVELOPMENT', + label: 'Business Development', + color: 'plum', + position: 16, + }, + { + key: 'chemical', + value: 'CHEMICAL', + label: 'Chemical', + color: 'grass', + position: 17, + }, + { + key: 'compliance', + value: 'COMPLIANCE', + label: 'Compliance', + color: 'tomato', + position: 18, + }, + { + key: 'construction', + value: 'CONSTRUCTION', + label: 'Construction', + color: 'iris', + position: 19, + }, + { + key: 'consulting', + value: 'CONSULTING', + label: 'Consulting', + color: 'mint', + position: 20, + }, + { + key: 'content', + value: 'CONTENT', + label: 'Content', + color: 'ruby', + position: 21, + }, + { + key: 'corporateDevelopment', + value: 'CORPORATE_DEVELOPMENT', + label: 'Corporate Development', + color: 'bronze', + position: 22, + }, + { + key: 'curation', + value: 'CURATION', + label: 'Curation', + color: 'jade', + position: 23, + }, + { + key: 'customerSuccess', + value: 'CUSTOMER_SUCCESS', + label: 'Customer Success', + color: 'gray', + position: 24, + }, + { + key: 'customerSupport', + value: 'CUSTOMER_SUPPORT', + label: 'Customer Support', + color: 'blue', + position: 25, + }, + { + key: 'dataAnalyst', + value: 'DATA_ANALYST', + label: 'Data Analyst', + color: 'red', + position: 26, + }, + { + key: 'dataEngineering', + value: 'DATA_ENGINEERING', + label: 'Data Engineering', + color: 'green', + position: 27, + }, + { + key: 'dataScience', + value: 'DATA_SCIENCE', + label: 'Data Science', + color: 'orange', + position: 28, + }, + { + key: 'dental', + value: 'DENTAL', + label: 'Dental', + color: 'purple', + position: 29, + }, + { + key: 'devops', + value: 'DEVOPS', + label: 'DevOps', + color: 'yellow', + position: 30, + }, + { + key: 'doctor', + value: 'DOCTOR', + label: 'Doctor', + color: 'pink', + position: 31, + }, + { + key: 'electric', + value: 'ELECTRIC', + label: 'Electric', + color: 'cyan', + position: 32, + }, + { + key: 'electrical', + value: 'ELECTRICAL', + label: 'Electrical', + color: 'brown', + position: 33, + }, + { + key: 'emergencyServices', + value: 'EMERGENCY_SERVICES', + label: 'Emergency Services', + color: 'lime', + position: 34, + }, + { + key: 'entertainment', + value: 'ENTERTAINMENT', + label: 'Entertainment', + color: 'violet', + position: 35, + }, + { + key: 'executive', + value: 'EXECUTIVE', + label: 'Executive', + color: 'gold', + position: 36, + }, + { + key: 'fashion', + value: 'FASHION', + label: 'Fashion', + color: 'turquoise', + position: 37, + }, + { + key: 'financial', + value: 'FINANCIAL', + label: 'Financial', + color: 'crimson', + position: 38, + }, + { + key: 'fitness', + value: 'FITNESS', + label: 'Fitness', + color: 'sky', + position: 39, + }, + { + key: 'fraud', + value: 'FRAUD', + label: 'Fraud', + color: 'amber', + position: 40, + }, + { + key: 'graphicDesign', + value: 'GRAPHIC_DESIGN', + label: 'Graphic Design', + color: 'plum', + position: 41, + }, + { + key: 'growth', + value: 'GROWTH', + label: 'Growth', + color: 'grass', + position: 42, + }, + { + key: 'hairStylist', + value: 'HAIR_STYLIST', + label: 'Hair Stylist', + color: 'tomato', + position: 43, + }, + { + key: 'hardware', + value: 'HARDWARE', + label: 'Hardware', + color: 'iris', + position: 44, + }, + { + key: 'healthAndSafety', + value: 'HEALTH_AND_SAFETY', + label: 'Health and Safety', + color: 'mint', + position: 45, + }, + { + key: 'humanResources', + value: 'HUMAN_RESOURCES', + label: 'Human Resources', + color: 'ruby', + position: 46, + }, + { + key: 'implementation', + value: 'IMPLEMENTATION', + label: 'Implementation', + color: 'bronze', + position: 47, + }, + { + key: 'industrial', + value: 'INDUSTRIAL', + label: 'Industrial', + color: 'jade', + position: 48, + }, + { + key: 'informationTechnology', + value: 'INFORMATION_TECHNOLOGY', + label: 'Information Technology', + color: 'gray', + position: 49, + }, + { + key: 'insurance', + value: 'INSURANCE', + label: 'Insurance', + color: 'blue', + position: 50, + }, + { + key: 'investmentBanking', + value: 'INVESTMENT_BANKING', + label: 'Investment Banking', + color: 'red', + position: 51, + }, + { + key: 'investor', + value: 'INVESTOR', + label: 'Investor', + color: 'green', + position: 52, + }, + { + key: 'investorRelations', + value: 'INVESTOR_RELATIONS', + label: 'Investor Relations', + color: 'orange', + position: 53, + }, + { + key: 'journalism', + value: 'JOURNALISM', + label: 'Journalism', + color: 'purple', + position: 54, + }, + { + key: 'judicial', + value: 'JUDICIAL', + label: 'Judicial', + color: 'yellow', + position: 55, + }, + { + key: 'legal', + value: 'LEGAL', + label: 'Legal', + color: 'pink', + position: 56, + }, + { + key: 'legalServices', + value: 'LEGAL_SERVICES', + label: 'Legal Services', + color: 'cyan', + position: 57, + }, + { + key: 'logistics', + value: 'LOGISTICS', + label: 'Logistics', + color: 'brown', + position: 58, + }, + { + key: 'machinist', + value: 'MACHINIST', + label: 'Machinist', + color: 'lime', + position: 59, + }, + { + key: 'marketingDesign', + value: 'MARKETING_DESIGN', + label: 'Marketing Design', + color: 'violet', + position: 60, + }, + { + key: 'marketingServices', + value: 'MARKETING_SERVICES', + label: 'Marketing Services', + color: 'gold', + position: 61, + }, + { + key: 'mechanic', + value: 'MECHANIC', + label: 'Mechanic', + color: 'turquoise', + position: 62, + }, + { + key: 'mechanical', + value: 'MECHANICAL', + label: 'Mechanical', + color: 'crimson', + position: 63, + }, + { + key: 'military', + value: 'MILITARY', + label: 'Military', + color: 'sky', + position: 64, + }, + { + key: 'network', + value: 'NETWORK', + label: 'Network', + color: 'amber', + position: 65, + }, + { + key: 'nursing', + value: 'NURSING', + label: 'Nursing', + color: 'plum', + position: 66, + }, + { + key: 'partnerships', + value: 'PARTNERSHIPS', + label: 'Partnerships', + color: 'grass', + position: 67, + }, + { + key: 'pharmacy', + value: 'PHARMACY', + label: 'Pharmacy', + color: 'tomato', + position: 68, + }, + { + key: 'planningAndAnalysis', + value: 'PLANNING_AND_ANALYSIS', + label: 'Planning and Analysis', + color: 'iris', + position: 69, + }, + { + key: 'plumbing', + value: 'PLUMBING', + label: 'Plumbing', + color: 'mint', + position: 70, + }, + { + key: 'political', + value: 'POLITICAL', + label: 'Political', + color: 'ruby', + position: 71, + }, + { + key: 'primaryAndSecondary', + value: 'PRIMARY_AND_SECONDARY', + label: 'Primary and Secondary', + color: 'bronze', + position: 72, + }, + { + key: 'procurement', + value: 'PROCUREMENT', + label: 'Procurement', + color: 'jade', + position: 73, + }, + { + key: 'productDesign', + value: 'PRODUCT_DESIGN', + label: 'Product Design', + color: 'gray', + position: 74, + }, + { + key: 'productManagement', + value: 'PRODUCT_MANAGEMENT', + label: 'Product Management', + color: 'blue', + position: 75, + }, + { + key: 'professor', + value: 'PROFESSOR', + label: 'Professor', + color: 'red', + position: 76, + }, + { + key: 'projectManagement', + value: 'PROJECT_MANAGEMENT', + label: 'Project Management', + color: 'green', + position: 77, + }, + { + key: 'protectiveService', + value: 'PROTECTIVE_SERVICE', + label: 'Protective Service', + color: 'orange', + position: 78, + }, + { + key: 'qaEngineering', + value: 'QA_ENGINEERING', + label: 'QA Engineering', + color: 'purple', + position: 79, + }, + { + key: 'qualityAssurance', + value: 'QUALITY_ASSURANCE', + label: 'Quality Assurance', + color: 'yellow', + position: 80, + }, + { + key: 'realtor', + value: 'REALTOR', + label: 'Realtor', + color: 'pink', + position: 81, + }, + { + key: 'recruiting', + value: 'RECRUITING', + label: 'Recruiting', + color: 'cyan', + position: 82, + }, + { + key: 'restaurants', + value: 'RESTAURANTS', + label: 'Restaurants', + color: 'brown', + position: 83, + }, + { + key: 'retail', + value: 'RETAIL', + label: 'Retail', + color: 'lime', + position: 84, + }, + { + key: 'revenueOperations', + value: 'REVENUE_OPERATIONS', + label: 'Revenue Operations', + color: 'violet', + position: 85, + }, + { + key: 'risk', + value: 'RISK', + label: 'Risk', + color: 'gold', + position: 86, + }, + { + key: 'salesDevelopment', + value: 'SALES_DEVELOPMENT', + label: 'Sales Development', + color: 'turquoise', + position: 87, + }, + { + key: 'scientific', + value: 'SCIENTIFIC', + label: 'Scientific', + color: 'crimson', + position: 88, + }, + { + key: 'security', + value: 'SECURITY', + label: 'Security', + color: 'sky', + position: 89, + }, + { + key: 'socialService', + value: 'SOCIAL_SERVICE', + label: 'Social Service', + color: 'amber', + position: 90, + }, + { + key: 'software', + value: 'SOFTWARE', + label: 'Software', + color: 'plum', + position: 91, + }, + { + key: 'solutionsEngineer', + value: 'SOLUTIONS_ENGINEER', + label: 'Solutions Engineer', + color: 'grass', + position: 92, + }, + { + key: 'strategy', + value: 'STRATEGY', + label: 'Strategy', + color: 'tomato', + position: 93, + }, + { + key: 'student', + value: 'STUDENT', + label: 'Student', + color: 'iris', + position: 94, + }, + { + key: 'talentAnalytics', + value: 'TALENT_ANALYTICS', + label: 'Talent Analytics', + color: 'mint', + position: 95, + }, + { + key: 'therapy', + value: 'THERAPY', + label: 'Therapy', + color: 'ruby', + position: 96, + }, + { + key: 'tourAndTravel', + value: 'TOUR_AND_TRAVEL', + label: 'Tour and Travel', + color: 'bronze', + position: 97, + }, + { + key: 'training', + value: 'TRAINING', + label: 'Training', + color: 'jade', + position: 98, + }, + { + key: 'translation', + value: 'TRANSLATION', + label: 'Translation', + color: 'gray', + position: 99, + }, + { + key: 'transport', + value: 'TRANSPORT', + label: 'Transport', + color: 'blue', + position: 100, + }, + { + key: 'unemployed', + value: 'UNEMPLOYED', + label: 'Unemployed', + color: 'red', + position: 101, + }, + { + key: 'veterinarian', + value: 'VETERINARIAN', + label: 'Veterinarian', + color: 'green', + position: 102, + }, + { + key: 'warehouse', + value: 'WAREHOUSE', + label: 'Warehouse', + color: 'orange', + position: 103, + }, + { + key: 'web', + value: 'WEB', + label: 'Web', + color: 'purple', + position: 104, + }, + { + key: 'wellness', + value: 'WELLNESS', + label: 'Wellness', + color: 'yellow', + position: 105, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/location-continent-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/location-continent-options.ts new file mode 100644 index 0000000000000..f876558407bb2 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/location-continent-options.ts @@ -0,0 +1,53 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const LOCATION_CONTINENT_OPTIONS: readonly SelectOptionMeta[] = [ + { + key: 'africa', + value: 'AFRICA', + label: 'Africa', + color: 'blue', + position: 0, + }, + { + key: 'antarctica', + value: 'ANTARCTICA', + label: 'Antarctica', + color: 'red', + position: 1, + }, + { + key: 'asia', + value: 'ASIA', + label: 'Asia', + color: 'green', + position: 2, + }, + { + key: 'europe', + value: 'EUROPE', + label: 'Europe', + color: 'orange', + position: 3, + }, + { + key: 'northAmerica', + value: 'NORTH_AMERICA', + label: 'North America', + color: 'purple', + position: 4, + }, + { + key: 'oceania', + value: 'OCEANIA', + label: 'Oceania', + color: 'yellow', + position: 5, + }, + { + key: 'southAmerica', + value: 'SOUTH_AMERICA', + label: 'South America', + color: 'pink', + position: 6, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/metro-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/metro-options.ts index cd2ad36694707..e7d35fccfc398 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/constants/metro-options.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/metro-options.ts @@ -1,689 +1,689 @@ -import { type SelectOptionMeta } from 'src/types/select-option-meta.type'; +import { type SelectOptionMeta } from 'src/types/select-option-meta'; export const METRO_OPTIONS: readonly SelectOptionMeta[] = [ { key: 'abileneTexas', value: 'ABILENE_TEXAS', - label: 'Abilene, Texas', + label: 'Abilene - Texas', color: 'blue', position: 0, }, { key: 'akronOhio', value: 'AKRON_OHIO', - label: 'Akron, Ohio', + label: 'Akron - Ohio', color: 'red', position: 1, }, { key: 'albanyGeorgia', value: 'ALBANY_GEORGIA', - label: 'Albany, Georgia', + label: 'Albany - Georgia', color: 'green', position: 2, }, { key: 'albanyNewYork', value: 'ALBANY_NEW_YORK', - label: 'Albany, New York', + label: 'Albany - New York', color: 'orange', position: 3, }, { key: 'albanyOregon', value: 'ALBANY_OREGON', - label: 'Albany, Oregon', + label: 'Albany - Oregon', color: 'purple', position: 4, }, { key: 'albuquerqueNewMexico', value: 'ALBUQUERQUE_NEW_MEXICO', - label: 'Albuquerque, New Mexico', + label: 'Albuquerque - New Mexico', color: 'yellow', position: 5, }, { key: 'alexandriaLouisiana', value: 'ALEXANDRIA_LOUISIANA', - label: 'Alexandria, Louisiana', + label: 'Alexandria - Louisiana', color: 'pink', position: 6, }, { key: 'allentownPennsylvania', value: 'ALLENTOWN_PENNSYLVANIA', - label: 'Allentown, Pennsylvania', + label: 'Allentown - Pennsylvania', color: 'cyan', position: 7, }, { key: 'altoonaPennsylvania', value: 'ALTOONA_PENNSYLVANIA', - label: 'Altoona, Pennsylvania', + label: 'Altoona - Pennsylvania', color: 'brown', position: 8, }, { key: 'amarilloTexas', value: 'AMARILLO_TEXAS', - label: 'Amarillo, Texas', + label: 'Amarillo - Texas', color: 'lime', position: 9, }, { key: 'amesIowa', value: 'AMES_IOWA', - label: 'Ames, Iowa', + label: 'Ames - Iowa', color: 'violet', position: 10, }, { key: 'anchorageAlaska', value: 'ANCHORAGE_ALASKA', - label: 'Anchorage, Alaska', + label: 'Anchorage - Alaska', color: 'gold', position: 11, }, { key: 'annArborMichigan', value: 'ANN_ARBOR_MICHIGAN', - label: 'Ann Arbor, Michigan', + label: 'Ann Arbor - Michigan', color: 'turquoise', position: 12, }, { key: 'annistonAlabama', value: 'ANNISTON_ALABAMA', - label: 'Anniston, Alabama', + label: 'Anniston - Alabama', color: 'crimson', position: 13, }, { key: 'appletonWisconsin', value: 'APPLETON_WISCONSIN', - label: 'Appleton, Wisconsin', + label: 'Appleton - Wisconsin', color: 'sky', position: 14, }, { key: 'ashevilleNorthCarolina', value: 'ASHEVILLE_NORTH_CAROLINA', - label: 'Asheville, North Carolina', + label: 'Asheville - North Carolina', color: 'amber', position: 15, }, { key: 'athensGeorgia', value: 'ATHENS_GEORGIA', - label: 'Athens, Georgia', + label: 'Athens - Georgia', color: 'plum', position: 16, }, { key: 'atlantaGeorgia', value: 'ATLANTA_GEORGIA', - label: 'Atlanta, Georgia', + label: 'Atlanta - Georgia', color: 'grass', position: 17, }, { key: 'atlanticCityNewJersey', value: 'ATLANTIC_CITY_NEW_JERSEY', - label: 'Atlantic City, New Jersey', + label: 'Atlantic City - New Jersey', color: 'tomato', position: 18, }, { key: 'auburnAlabama', value: 'AUBURN_ALABAMA', - label: 'Auburn, Alabama', + label: 'Auburn - Alabama', color: 'iris', position: 19, }, { key: 'augustaGeorgia', value: 'AUGUSTA_GEORGIA', - label: 'Augusta, Georgia', + label: 'Augusta - Georgia', color: 'mint', position: 20, }, { key: 'austinTexas', value: 'AUSTIN_TEXAS', - label: 'Austin, Texas', + label: 'Austin - Texas', color: 'ruby', position: 21, }, { key: 'bakersfieldCalifornia', value: 'BAKERSFIELD_CALIFORNIA', - label: 'Bakersfield, California', + label: 'Bakersfield - California', color: 'bronze', position: 22, }, { key: 'baltimoreMaryland', value: 'BALTIMORE_MARYLAND', - label: 'Baltimore, Maryland', + label: 'Baltimore - Maryland', color: 'jade', position: 23, }, { key: 'bangorMaine', value: 'BANGOR_MAINE', - label: 'Bangor, Maine', + label: 'Bangor - Maine', color: 'gray', position: 24, }, { key: 'barnstableTownMassachusetts', value: 'BARNSTABLE_TOWN_MASSACHUSETTS', - label: 'Barnstable Town, Massachusetts', + label: 'Barnstable Town - Massachusetts', color: 'blue', position: 25, }, { key: 'batonRougeLouisiana', value: 'BATON_ROUGE_LOUISIANA', - label: 'Baton Rouge, Louisiana', + label: 'Baton Rouge - Louisiana', color: 'red', position: 26, }, { key: 'battleCreekMichigan', value: 'BATTLE_CREEK_MICHIGAN', - label: 'Battle Creek, Michigan', + label: 'Battle Creek - Michigan', color: 'green', position: 27, }, { key: 'bayCityMichigan', value: 'BAY_CITY_MICHIGAN', - label: 'Bay City, Michigan', + label: 'Bay City - Michigan', color: 'orange', position: 28, }, { key: 'beaumontTexas', value: 'BEAUMONT_TEXAS', - label: 'Beaumont, Texas', + label: 'Beaumont - Texas', color: 'purple', position: 29, }, { key: 'beckleyWestVirginia', value: 'BECKLEY_WEST_VIRGINIA', - label: 'Beckley, West Virginia', + label: 'Beckley - West Virginia', color: 'yellow', position: 30, }, { key: 'bellinghamWashington', value: 'BELLINGHAM_WASHINGTON', - label: 'Bellingham, Washington', + label: 'Bellingham - Washington', color: 'pink', position: 31, }, { key: 'bendOregon', value: 'BEND_OREGON', - label: 'Bend, Oregon', + label: 'Bend - Oregon', color: 'cyan', position: 32, }, { key: 'billingsMontana', value: 'BILLINGS_MONTANA', - label: 'Billings, Montana', + label: 'Billings - Montana', color: 'brown', position: 33, }, { key: 'binghamtonNewYork', value: 'BINGHAMTON_NEW_YORK', - label: 'Binghamton, New York', + label: 'Binghamton - New York', color: 'lime', position: 34, }, { key: 'birminghamAlabama', value: 'BIRMINGHAM_ALABAMA', - label: 'Birmingham, Alabama', + label: 'Birmingham - Alabama', color: 'violet', position: 35, }, { key: 'bismarckNorthDakota', value: 'BISMARCK_NORTH_DAKOTA', - label: 'Bismarck, North Dakota', + label: 'Bismarck - North Dakota', color: 'gold', position: 36, }, { key: 'blacksburgVirginia', value: 'BLACKSBURG_VIRGINIA', - label: 'Blacksburg, Virginia', + label: 'Blacksburg - Virginia', color: 'turquoise', position: 37, }, { key: 'bloomingtonIllinois', value: 'BLOOMINGTON_ILLINOIS', - label: 'Bloomington, Illinois', + label: 'Bloomington - Illinois', color: 'crimson', position: 38, }, { key: 'bloomingtonIndiana', value: 'BLOOMINGTON_INDIANA', - label: 'Bloomington, Indiana', + label: 'Bloomington - Indiana', color: 'sky', position: 39, }, { key: 'bloomsburgPennsylvania', value: 'BLOOMSBURG_PENNSYLVANIA', - label: 'Bloomsburg, Pennsylvania', + label: 'Bloomsburg - Pennsylvania', color: 'amber', position: 40, }, { key: 'boiseCityIdaho', value: 'BOISE_CITY_IDAHO', - label: 'Boise City, Idaho', + label: 'Boise City - Idaho', color: 'plum', position: 41, }, { key: 'bostonMassachusetts', value: 'BOSTON_MASSACHUSETTS', - label: 'Boston, Massachusetts', + label: 'Boston - Massachusetts', color: 'grass', position: 42, }, { key: 'boulderColorado', value: 'BOULDER_COLORADO', - label: 'Boulder, Colorado', + label: 'Boulder - Colorado', color: 'tomato', position: 43, }, { key: 'bowlingGreenKentucky', value: 'BOWLING_GREEN_KENTUCKY', - label: 'Bowling Green, Kentucky', + label: 'Bowling Green - Kentucky', color: 'iris', position: 44, }, { key: 'bremertonWashington', value: 'BREMERTON_WASHINGTON', - label: 'Bremerton, Washington', + label: 'Bremerton - Washington', color: 'mint', position: 45, }, { key: 'bridgeportConnecticut', value: 'BRIDGEPORT_CONNECTICUT', - label: 'Bridgeport, Connecticut', + label: 'Bridgeport - Connecticut', color: 'ruby', position: 46, }, { key: 'brownsvilleTexas', value: 'BROWNSVILLE_TEXAS', - label: 'Brownsville, Texas', + label: 'Brownsville - Texas', color: 'bronze', position: 47, }, { key: 'brunswickGeorgia', value: 'BRUNSWICK_GEORGIA', - label: 'Brunswick, Georgia', + label: 'Brunswick - Georgia', color: 'jade', position: 48, }, { key: 'buffaloNewYork', value: 'BUFFALO_NEW_YORK', - label: 'Buffalo, New York', + label: 'Buffalo - New York', color: 'gray', position: 49, }, { key: 'burlingtonNorthCarolina', value: 'BURLINGTON_NORTH_CAROLINA', - label: 'Burlington, North Carolina', + label: 'Burlington - North Carolina', color: 'blue', position: 50, }, { key: 'burlingtonVermont', value: 'BURLINGTON_VERMONT', - label: 'Burlington, Vermont', + label: 'Burlington - Vermont', color: 'red', position: 51, }, { key: 'californiaMaryland', value: 'CALIFORNIA_MARYLAND', - label: 'California, Maryland', + label: 'California - Maryland', color: 'green', position: 52, }, { key: 'cantonOhio', value: 'CANTON_OHIO', - label: 'Canton, Ohio', + label: 'Canton - Ohio', color: 'orange', position: 53, }, { key: 'capeCoralFlorida', value: 'CAPE_CORAL_FLORIDA', - label: 'Cape Coral, Florida', + label: 'Cape Coral - Florida', color: 'purple', position: 54, }, { key: 'capeGirardeauMissouri', value: 'CAPE_GIRARDEAU_MISSOURI', - label: 'Cape Girardeau, Missouri', + label: 'Cape Girardeau - Missouri', color: 'yellow', position: 55, }, { key: 'carbondaleIllinois', value: 'CARBONDALE_ILLINOIS', - label: 'Carbondale, Illinois', + label: 'Carbondale - Illinois', color: 'pink', position: 56, }, { key: 'carsonCityNevada', value: 'CARSON_CITY_NEVADA', - label: 'Carson City, Nevada', + label: 'Carson City - Nevada', color: 'cyan', position: 57, }, { key: 'casperWyoming', value: 'CASPER_WYOMING', - label: 'Casper, Wyoming', + label: 'Casper - Wyoming', color: 'brown', position: 58, }, { key: 'cedarRapidsIowa', value: 'CEDAR_RAPIDS_IOWA', - label: 'Cedar Rapids, Iowa', + label: 'Cedar Rapids - Iowa', color: 'lime', position: 59, }, { key: 'chambersburgPennsylvania', value: 'CHAMBERSBURG_PENNSYLVANIA', - label: 'Chambersburg, Pennsylvania', + label: 'Chambersburg - Pennsylvania', color: 'violet', position: 60, }, { key: 'champaignIllinois', value: 'CHAMPAIGN_ILLINOIS', - label: 'Champaign, Illinois', + label: 'Champaign - Illinois', color: 'gold', position: 61, }, { key: 'charlestonSouthCarolina', value: 'CHARLESTON_SOUTH_CAROLINA', - label: 'Charleston, South Carolina', + label: 'Charleston - South Carolina', color: 'turquoise', position: 62, }, { key: 'charlestonWestVirginia', value: 'CHARLESTON_WEST_VIRGINIA', - label: 'Charleston, West Virginia', + label: 'Charleston - West Virginia', color: 'crimson', position: 63, }, { key: 'charlotteNorthCarolina', value: 'CHARLOTTE_NORTH_CAROLINA', - label: 'Charlotte, North Carolina', + label: 'Charlotte - North Carolina', color: 'sky', position: 64, }, { key: 'charlottesvilleVirginia', value: 'CHARLOTTESVILLE_VIRGINIA', - label: 'Charlottesville, Virginia', + label: 'Charlottesville - Virginia', color: 'amber', position: 65, }, { key: 'chattanoogaTennessee', value: 'CHATTANOOGA_TENNESSEE', - label: 'Chattanooga, Tennessee', + label: 'Chattanooga - Tennessee', color: 'plum', position: 66, }, { key: 'cheyenneWyoming', value: 'CHEYENNE_WYOMING', - label: 'Cheyenne, Wyoming', + label: 'Cheyenne - Wyoming', color: 'grass', position: 67, }, { key: 'chicagoIllinois', value: 'CHICAGO_ILLINOIS', - label: 'Chicago, Illinois', + label: 'Chicago - Illinois', color: 'tomato', position: 68, }, { key: 'chicoCalifornia', value: 'CHICO_CALIFORNIA', - label: 'Chico, California', + label: 'Chico - California', color: 'iris', position: 69, }, { key: 'cincinnatiOhio', value: 'CINCINNATI_OHIO', - label: 'Cincinnati, Ohio', + label: 'Cincinnati - Ohio', color: 'mint', position: 70, }, { key: 'clarksvilleTennessee', value: 'CLARKSVILLE_TENNESSEE', - label: 'Clarksville, Tennessee', + label: 'Clarksville - Tennessee', color: 'ruby', position: 71, }, { key: 'clevelandOhio', value: 'CLEVELAND_OHIO', - label: 'Cleveland, Ohio', + label: 'Cleveland - Ohio', color: 'bronze', position: 72, }, { key: 'clevelandTennessee', value: 'CLEVELAND_TENNESSEE', - label: 'Cleveland, Tennessee', + label: 'Cleveland - Tennessee', color: 'jade', position: 73, }, { key: 'coeurDAleneIdaho', value: 'COEUR_D_ALENE_IDAHO', - label: "Coeur d'Alene, Idaho", + label: "Coeur d'Alene - Idaho", color: 'gray', position: 74, }, { key: 'collegeStationTexas', value: 'COLLEGE_STATION_TEXAS', - label: 'College Station, Texas', + label: 'College Station - Texas', color: 'blue', position: 75, }, { key: 'coloradoSpringsColorado', value: 'COLORADO_SPRINGS_COLORADO', - label: 'Colorado Springs, Colorado', + label: 'Colorado Springs - Colorado', color: 'red', position: 76, }, { key: 'columbiaMissouri', value: 'COLUMBIA_MISSOURI', - label: 'Columbia, Missouri', + label: 'Columbia - Missouri', color: 'green', position: 77, }, { key: 'columbiaSouthCarolina', value: 'COLUMBIA_SOUTH_CAROLINA', - label: 'Columbia, South Carolina', + label: 'Columbia - South Carolina', color: 'orange', position: 78, }, { key: 'columbusGeorgia', value: 'COLUMBUS_GEORGIA', - label: 'Columbus, Georgia', + label: 'Columbus - Georgia', color: 'purple', position: 79, }, { key: 'columbusIndiana', value: 'COLUMBUS_INDIANA', - label: 'Columbus, Indiana', + label: 'Columbus - Indiana', color: 'yellow', position: 80, }, { key: 'columbusOhio', value: 'COLUMBUS_OHIO', - label: 'Columbus, Ohio', + label: 'Columbus - Ohio', color: 'pink', position: 81, }, { key: 'corpusChristiTexas', value: 'CORPUS_CHRISTI_TEXAS', - label: 'Corpus Christi, Texas', + label: 'Corpus Christi - Texas', color: 'cyan', position: 82, }, { key: 'corvallisOregon', value: 'CORVALLIS_OREGON', - label: 'Corvallis, Oregon', + label: 'Corvallis - Oregon', color: 'brown', position: 83, }, { key: 'crestviewFlorida', value: 'CRESTVIEW_FLORIDA', - label: 'Crestview, Florida', + label: 'Crestview - Florida', color: 'lime', position: 84, }, { key: 'cumberlandMaryland', value: 'CUMBERLAND_MARYLAND', - label: 'Cumberland, Maryland', + label: 'Cumberland - Maryland', color: 'violet', position: 85, }, { key: 'dallasTexas', value: 'DALLAS_TEXAS', - label: 'Dallas, Texas', + label: 'Dallas - Texas', color: 'gold', position: 86, }, { key: 'daltonGeorgia', value: 'DALTON_GEORGIA', - label: 'Dalton, Georgia', + label: 'Dalton - Georgia', color: 'turquoise', position: 87, }, { key: 'danvilleIllinois', value: 'DANVILLE_ILLINOIS', - label: 'Danville, Illinois', + label: 'Danville - Illinois', color: 'crimson', position: 88, }, { key: 'daphneAlabama', value: 'DAPHNE_ALABAMA', - label: 'Daphne, Alabama', + label: 'Daphne - Alabama', color: 'sky', position: 89, }, { key: 'davenportIowa', value: 'DAVENPORT_IOWA', - label: 'Davenport, Iowa', + label: 'Davenport - Iowa', color: 'amber', position: 90, }, { key: 'daytonOhio', value: 'DAYTON_OHIO', - label: 'Dayton, Ohio', + label: 'Dayton - Ohio', color: 'plum', position: 91, }, { key: 'decaturAlabama', value: 'DECATUR_ALABAMA', - label: 'Decatur, Alabama', + label: 'Decatur - Alabama', color: 'grass', position: 92, }, { key: 'decaturIllinois', value: 'DECATUR_ILLINOIS', - label: 'Decatur, Illinois', + label: 'Decatur - Illinois', color: 'tomato', position: 93, }, { key: 'deltonaFlorida', value: 'DELTONA_FLORIDA', - label: 'Deltona, Florida', + label: 'Deltona - Florida', color: 'iris', position: 94, }, { key: 'denverColorado', value: 'DENVER_COLORADO', - label: 'Denver, Colorado', + label: 'Denver - Colorado', color: 'mint', position: 95, }, { key: 'desMoinesIowa', value: 'DES_MOINES_IOWA', - label: 'Des Moines, Iowa', + label: 'Des Moines - Iowa', color: 'ruby', position: 96, }, { key: 'detroitMichigan', value: 'DETROIT_MICHIGAN', - label: 'Detroit, Michigan', + label: 'Detroit - Michigan', color: 'bronze', position: 97, }, @@ -697,1995 +697,1995 @@ export const METRO_OPTIONS: readonly SelectOptionMeta[] = [ { key: 'dothanAlabama', value: 'DOTHAN_ALABAMA', - label: 'Dothan, Alabama', + label: 'Dothan - Alabama', color: 'gray', position: 99, }, { key: 'doverDelaware', value: 'DOVER_DELAWARE', - label: 'Dover, Delaware', + label: 'Dover - Delaware', color: 'blue', position: 100, }, { key: 'dubuqueIowa', value: 'DUBUQUE_IOWA', - label: 'Dubuque, Iowa', + label: 'Dubuque - Iowa', color: 'red', position: 101, }, { key: 'duluthMinnesota', value: 'DULUTH_MINNESOTA', - label: 'Duluth, Minnesota', + label: 'Duluth - Minnesota', color: 'green', position: 102, }, { key: 'durhamNorthCarolina', value: 'DURHAM_NORTH_CAROLINA', - label: 'Durham, North Carolina', + label: 'Durham - North Carolina', color: 'orange', position: 103, }, { key: 'eastStroudsburgPennsylvania', value: 'EAST_STROUDSBURG_PENNSYLVANIA', - label: 'East Stroudsburg, Pennsylvania', + label: 'East Stroudsburg - Pennsylvania', color: 'purple', position: 104, }, { key: 'eauClaireWisconsin', value: 'EAU_CLAIRE_WISCONSIN', - label: 'Eau Claire, Wisconsin', + label: 'Eau Claire - Wisconsin', color: 'yellow', position: 105, }, { key: 'elCentroCalifornia', value: 'EL_CENTRO_CALIFORNIA', - label: 'El Centro, California', + label: 'El Centro - California', color: 'pink', position: 106, }, { key: 'elPasoTexas', value: 'EL_PASO_TEXAS', - label: 'El Paso, Texas', + label: 'El Paso - Texas', color: 'cyan', position: 107, }, { key: 'elizabethtownKentucky', value: 'ELIZABETHTOWN_KENTUCKY', - label: 'Elizabethtown, Kentucky', + label: 'Elizabethtown - Kentucky', color: 'brown', position: 108, }, { key: 'elkhartIndiana', value: 'ELKHART_INDIANA', - label: 'Elkhart, Indiana', + label: 'Elkhart - Indiana', color: 'lime', position: 109, }, { key: 'elmiraNewYork', value: 'ELMIRA_NEW_YORK', - label: 'Elmira, New York', + label: 'Elmira - New York', color: 'violet', position: 110, }, { key: 'enidOklahoma', value: 'ENID_OKLAHOMA', - label: 'Enid, Oklahoma', + label: 'Enid - Oklahoma', color: 'gold', position: 111, }, { key: 'eriePennsylvania', value: 'ERIE_PENNSYLVANIA', - label: 'Erie, Pennsylvania', + label: 'Erie - Pennsylvania', color: 'turquoise', position: 112, }, { key: 'eugeneOregon', value: 'EUGENE_OREGON', - label: 'Eugene, Oregon', + label: 'Eugene - Oregon', color: 'crimson', position: 113, }, { key: 'evansvilleIndiana', value: 'EVANSVILLE_INDIANA', - label: 'Evansville, Indiana', + label: 'Evansville - Indiana', color: 'sky', position: 114, }, { key: 'fairbanksAlaska', value: 'FAIRBANKS_ALASKA', - label: 'Fairbanks, Alaska', + label: 'Fairbanks - Alaska', color: 'amber', position: 115, }, { key: 'fargoNorthDakota', value: 'FARGO_NORTH_DAKOTA', - label: 'Fargo, North Dakota', + label: 'Fargo - North Dakota', color: 'plum', position: 116, }, { key: 'farmingtonNewMexico', value: 'FARMINGTON_NEW_MEXICO', - label: 'Farmington, New Mexico', + label: 'Farmington - New Mexico', color: 'grass', position: 117, }, { key: 'fayettevilleArkansas', value: 'FAYETTEVILLE_ARKANSAS', - label: 'Fayetteville, Arkansas', + label: 'Fayetteville - Arkansas', color: 'tomato', position: 118, }, { key: 'fayettevilleNorthCarolina', value: 'FAYETTEVILLE_NORTH_CAROLINA', - label: 'Fayetteville, North Carolina', + label: 'Fayetteville - North Carolina', color: 'iris', position: 119, }, { key: 'flagstaffArizona', value: 'FLAGSTAFF_ARIZONA', - label: 'Flagstaff, Arizona', + label: 'Flagstaff - Arizona', color: 'mint', position: 120, }, { key: 'flintMichigan', value: 'FLINT_MICHIGAN', - label: 'Flint, Michigan', + label: 'Flint - Michigan', color: 'ruby', position: 121, }, { key: 'florenceAlabama', value: 'FLORENCE_ALABAMA', - label: 'Florence, Alabama', + label: 'Florence - Alabama', color: 'bronze', position: 122, }, { key: 'florenceSouthCarolina', value: 'FLORENCE_SOUTH_CAROLINA', - label: 'Florence, South Carolina', + label: 'Florence - South Carolina', color: 'jade', position: 123, }, { key: 'fondDuLacWisconsin', value: 'FOND_DU_LAC_WISCONSIN', - label: 'Fond Du Lac, Wisconsin', + label: 'Fond Du Lac - Wisconsin', color: 'gray', position: 124, }, { key: 'fortCollinsColorado', value: 'FORT_COLLINS_COLORADO', - label: 'Fort Collins, Colorado', + label: 'Fort Collins - Colorado', color: 'blue', position: 125, }, { key: 'fortSmithArkansas', value: 'FORT_SMITH_ARKANSAS', - label: 'Fort Smith, Arkansas', + label: 'Fort Smith - Arkansas', color: 'red', position: 126, }, { key: 'fortWayneIndiana', value: 'FORT_WAYNE_INDIANA', - label: 'Fort Wayne, Indiana', + label: 'Fort Wayne - Indiana', color: 'green', position: 127, }, { key: 'fresnoCalifornia', value: 'FRESNO_CALIFORNIA', - label: 'Fresno, California', + label: 'Fresno - California', color: 'orange', position: 128, }, { key: 'gadsdenAlabama', value: 'GADSDEN_ALABAMA', - label: 'Gadsden, Alabama', + label: 'Gadsden - Alabama', color: 'purple', position: 129, }, { key: 'gainesvilleFlorida', value: 'GAINESVILLE_FLORIDA', - label: 'Gainesville, Florida', + label: 'Gainesville - Florida', color: 'yellow', position: 130, }, { key: 'gainesvilleGeorgia', value: 'GAINESVILLE_GEORGIA', - label: 'Gainesville, Georgia', + label: 'Gainesville - Georgia', color: 'pink', position: 131, }, { key: 'gettysburgPennsylvania', value: 'GETTYSBURG_PENNSYLVANIA', - label: 'Gettysburg, Pennsylvania', + label: 'Gettysburg - Pennsylvania', color: 'cyan', position: 132, }, { key: 'glensFallsNewYork', value: 'GLENS_FALLS_NEW_YORK', - label: 'Glens Falls, New York', + label: 'Glens Falls - New York', color: 'brown', position: 133, }, { key: 'goldsboroNorthCarolina', value: 'GOLDSBORO_NORTH_CAROLINA', - label: 'Goldsboro, North Carolina', + label: 'Goldsboro - North Carolina', color: 'lime', position: 134, }, { key: 'grandForksNorthDakota', value: 'GRAND_FORKS_NORTH_DAKOTA', - label: 'Grand Forks, North Dakota', + label: 'Grand Forks - North Dakota', color: 'violet', position: 135, }, { key: 'grandIslandNebraska', value: 'GRAND_ISLAND_NEBRASKA', - label: 'Grand Island, Nebraska', + label: 'Grand Island - Nebraska', color: 'gold', position: 136, }, { key: 'grandJunctionColorado', value: 'GRAND_JUNCTION_COLORADO', - label: 'Grand Junction, Colorado', + label: 'Grand Junction - Colorado', color: 'turquoise', position: 137, }, { key: 'grandRapidsMichigan', value: 'GRAND_RAPIDS_MICHIGAN', - label: 'Grand Rapids, Michigan', + label: 'Grand Rapids - Michigan', color: 'crimson', position: 138, }, { key: 'grantsPassOregon', value: 'GRANTS_PASS_OREGON', - label: 'Grants Pass, Oregon', + label: 'Grants Pass - Oregon', color: 'sky', position: 139, }, { key: 'greatFallsMontana', value: 'GREAT_FALLS_MONTANA', - label: 'Great Falls, Montana', + label: 'Great Falls - Montana', color: 'amber', position: 140, }, { key: 'greeleyColorado', value: 'GREELEY_COLORADO', - label: 'Greeley, Colorado', + label: 'Greeley - Colorado', color: 'plum', position: 141, }, { key: 'greenBayWisconsin', value: 'GREEN_BAY_WISCONSIN', - label: 'Green Bay, Wisconsin', + label: 'Green Bay - Wisconsin', color: 'grass', position: 142, }, { key: 'greensboroNorthCarolina', value: 'GREENSBORO_NORTH_CAROLINA', - label: 'Greensboro, North Carolina', + label: 'Greensboro - North Carolina', color: 'tomato', position: 143, }, { key: 'greenvilleNorthCarolina', value: 'GREENVILLE_NORTH_CAROLINA', - label: 'Greenville, North Carolina', + label: 'Greenville - North Carolina', color: 'iris', position: 144, }, { key: 'greenvilleSouthCarolina', value: 'GREENVILLE_SOUTH_CAROLINA', - label: 'Greenville, South Carolina', + label: 'Greenville - South Carolina', color: 'mint', position: 145, }, { key: 'gulfportMississippi', value: 'GULFPORT_MISSISSIPPI', - label: 'Gulfport, Mississippi', + label: 'Gulfport - Mississippi', color: 'ruby', position: 146, }, { key: 'hagerstownMaryland', value: 'HAGERSTOWN_MARYLAND', - label: 'Hagerstown, Maryland', + label: 'Hagerstown - Maryland', color: 'bronze', position: 147, }, { key: 'hammondLouisiana', value: 'HAMMOND_LOUISIANA', - label: 'Hammond, Louisiana', + label: 'Hammond - Louisiana', color: 'jade', position: 148, }, { key: 'hanfordCalifornia', value: 'HANFORD_CALIFORNIA', - label: 'Hanford, California', + label: 'Hanford - California', color: 'gray', position: 149, }, { key: 'harrisburgPennsylvania', value: 'HARRISBURG_PENNSYLVANIA', - label: 'Harrisburg, Pennsylvania', + label: 'Harrisburg - Pennsylvania', color: 'blue', position: 150, }, { key: 'harrisonburgVirginia', value: 'HARRISONBURG_VIRGINIA', - label: 'Harrisonburg, Virginia', + label: 'Harrisonburg - Virginia', color: 'red', position: 151, }, { key: 'hartfordConnecticut', value: 'HARTFORD_CONNECTICUT', - label: 'Hartford, Connecticut', + label: 'Hartford - Connecticut', color: 'green', position: 152, }, { key: 'hattiesburgMississippi', value: 'HATTIESBURG_MISSISSIPPI', - label: 'Hattiesburg, Mississippi', + label: 'Hattiesburg - Mississippi', color: 'orange', position: 153, }, { key: 'hickoryNorthCarolina', value: 'HICKORY_NORTH_CAROLINA', - label: 'Hickory, North Carolina', + label: 'Hickory - North Carolina', color: 'purple', position: 154, }, { key: 'hiltonHeadIslandSouthCarolina', value: 'HILTON_HEAD_ISLAND_SOUTH_CAROLINA', - label: 'Hilton Head Island, South Carolina', + label: 'Hilton Head Island - South Carolina', color: 'yellow', position: 155, }, { key: 'hinesvilleGeorgia', value: 'HINESVILLE_GEORGIA', - label: 'Hinesville, Georgia', + label: 'Hinesville - Georgia', color: 'pink', position: 156, }, { key: 'homosassaSpringsFlorida', value: 'HOMOSASSA_SPRINGS_FLORIDA', - label: 'Homosassa Springs, Florida', + label: 'Homosassa Springs - Florida', color: 'cyan', position: 157, }, { key: 'hotSpringsArkansas', value: 'HOT_SPRINGS_ARKANSAS', - label: 'Hot Springs, Arkansas', + label: 'Hot Springs - Arkansas', color: 'brown', position: 158, }, { key: 'houmaLouisiana', value: 'HOUMA_LOUISIANA', - label: 'Houma, Louisiana', + label: 'Houma - Louisiana', color: 'lime', position: 159, }, { key: 'houstonTexas', value: 'HOUSTON_TEXAS', - label: 'Houston, Texas', + label: 'Houston - Texas', color: 'violet', position: 160, }, { key: 'huntingtonWestVirginia', value: 'HUNTINGTON_WEST_VIRGINIA', - label: 'Huntington, West Virginia', + label: 'Huntington - West Virginia', color: 'gold', position: 161, }, { key: 'huntsvilleAlabama', value: 'HUNTSVILLE_ALABAMA', - label: 'Huntsville, Alabama', + label: 'Huntsville - Alabama', color: 'turquoise', position: 162, }, { key: 'idahoFallsIdaho', value: 'IDAHO_FALLS_IDAHO', - label: 'Idaho Falls, Idaho', + label: 'Idaho Falls - Idaho', color: 'crimson', position: 163, }, { key: 'indianapolisIndiana', value: 'INDIANAPOLIS_INDIANA', - label: 'Indianapolis, Indiana', + label: 'Indianapolis - Indiana', color: 'sky', position: 164, }, { key: 'iowaCityIowa', value: 'IOWA_CITY_IOWA', - label: 'Iowa City, Iowa', + label: 'Iowa City - Iowa', color: 'amber', position: 165, }, { key: 'ithacaNewYork', value: 'ITHACA_NEW_YORK', - label: 'Ithaca, New York', + label: 'Ithaca - New York', color: 'plum', position: 166, }, { key: 'jacksonMichigan', value: 'JACKSON_MICHIGAN', - label: 'Jackson, Michigan', + label: 'Jackson - Michigan', color: 'grass', position: 167, }, { key: 'jacksonMississippi', value: 'JACKSON_MISSISSIPPI', - label: 'Jackson, Mississippi', + label: 'Jackson - Mississippi', color: 'tomato', position: 168, }, { key: 'jacksonTennessee', value: 'JACKSON_TENNESSEE', - label: 'Jackson, Tennessee', + label: 'Jackson - Tennessee', color: 'iris', position: 169, }, { key: 'jacksonvilleFlorida', value: 'JACKSONVILLE_FLORIDA', - label: 'Jacksonville, Florida', + label: 'Jacksonville - Florida', color: 'mint', position: 170, }, { key: 'jacksonvilleNorthCarolina', value: 'JACKSONVILLE_NORTH_CAROLINA', - label: 'Jacksonville, North Carolina', + label: 'Jacksonville - North Carolina', color: 'ruby', position: 171, }, { key: 'janesvilleWisconsin', value: 'JANESVILLE_WISCONSIN', - label: 'Janesville, Wisconsin', + label: 'Janesville - Wisconsin', color: 'bronze', position: 172, }, { key: 'jeffersonCityMissouri', value: 'JEFFERSON_CITY_MISSOURI', - label: 'Jefferson City, Missouri', + label: 'Jefferson City - Missouri', color: 'jade', position: 173, }, { key: 'johnsonCityTennessee', value: 'JOHNSON_CITY_TENNESSEE', - label: 'Johnson City, Tennessee', + label: 'Johnson City - Tennessee', color: 'gray', position: 174, }, { key: 'johnstownPennsylvania', value: 'JOHNSTOWN_PENNSYLVANIA', - label: 'Johnstown, Pennsylvania', + label: 'Johnstown - Pennsylvania', color: 'blue', position: 175, }, { key: 'jonesboroArkansas', value: 'JONESBORO_ARKANSAS', - label: 'Jonesboro, Arkansas', + label: 'Jonesboro - Arkansas', color: 'red', position: 176, }, { key: 'joplinMissouri', value: 'JOPLIN_MISSOURI', - label: 'Joplin, Missouri', + label: 'Joplin - Missouri', color: 'green', position: 177, }, { key: 'kahuluiHawaii', value: 'KAHULUI_HAWAII', - label: 'Kahului, Hawaii', + label: 'Kahului - Hawaii', color: 'orange', position: 178, }, { key: 'kalamazooMichigan', value: 'KALAMAZOO_MICHIGAN', - label: 'Kalamazoo, Michigan', + label: 'Kalamazoo - Michigan', color: 'purple', position: 179, }, { key: 'kankakeeIllinois', value: 'KANKAKEE_ILLINOIS', - label: 'Kankakee, Illinois', + label: 'Kankakee - Illinois', color: 'yellow', position: 180, }, { key: 'kansasCityMissouri', value: 'KANSAS_CITY_MISSOURI', - label: 'Kansas City, Missouri', + label: 'Kansas City - Missouri', color: 'pink', position: 181, }, { key: 'kennewickWashington', value: 'KENNEWICK_WASHINGTON', - label: 'Kennewick, Washington', + label: 'Kennewick - Washington', color: 'cyan', position: 182, }, { key: 'killeenTexas', value: 'KILLEEN_TEXAS', - label: 'Killeen, Texas', + label: 'Killeen - Texas', color: 'brown', position: 183, }, { key: 'kingsportTennessee', value: 'KINGSPORT_TENNESSEE', - label: 'Kingsport, Tennessee', + label: 'Kingsport - Tennessee', color: 'lime', position: 184, }, { key: 'kingstonNewYork', value: 'KINGSTON_NEW_YORK', - label: 'Kingston, New York', + label: 'Kingston - New York', color: 'violet', position: 185, }, { key: 'knoxvilleTennessee', value: 'KNOXVILLE_TENNESSEE', - label: 'Knoxville, Tennessee', + label: 'Knoxville - Tennessee', color: 'gold', position: 186, }, { key: 'kokomoIndiana', value: 'KOKOMO_INDIANA', - label: 'Kokomo, Indiana', + label: 'Kokomo - Indiana', color: 'turquoise', position: 187, }, { key: 'laCrosseWisconsin', value: 'LA_CROSSE_WISCONSIN', - label: 'La Crosse, Wisconsin', + label: 'La Crosse - Wisconsin', color: 'crimson', position: 188, }, { key: 'lafayetteIndiana', value: 'LAFAYETTE_INDIANA', - label: 'Lafayette, Indiana', + label: 'Lafayette - Indiana', color: 'sky', position: 189, }, { key: 'lafayetteLouisiana', value: 'LAFAYETTE_LOUISIANA', - label: 'Lafayette, Louisiana', + label: 'Lafayette - Louisiana', color: 'amber', position: 190, }, { key: 'lakeCharlesLouisiana', value: 'LAKE_CHARLES_LOUISIANA', - label: 'Lake Charles, Louisiana', + label: 'Lake Charles - Louisiana', color: 'plum', position: 191, }, { key: 'lakeHavasuCityArizona', value: 'LAKE_HAVASU_CITY_ARIZONA', - label: 'Lake Havasu City, Arizona', + label: 'Lake Havasu City - Arizona', color: 'grass', position: 192, }, { key: 'lakelandFlorida', value: 'LAKELAND_FLORIDA', - label: 'Lakeland, Florida', + label: 'Lakeland - Florida', color: 'tomato', position: 193, }, { key: 'lancasterPennsylvania', value: 'LANCASTER_PENNSYLVANIA', - label: 'Lancaster, Pennsylvania', + label: 'Lancaster - Pennsylvania', color: 'iris', position: 194, }, { key: 'lansingMichigan', value: 'LANSING_MICHIGAN', - label: 'Lansing, Michigan', + label: 'Lansing - Michigan', color: 'mint', position: 195, }, { key: 'laredoTexas', value: 'LAREDO_TEXAS', - label: 'Laredo, Texas', + label: 'Laredo - Texas', color: 'ruby', position: 196, }, { key: 'lasCrucesNewMexico', value: 'LAS_CRUCES_NEW_MEXICO', - label: 'Las Cruces, New Mexico', + label: 'Las Cruces - New Mexico', color: 'bronze', position: 197, }, { key: 'lasVegasNevada', value: 'LAS_VEGAS_NEVADA', - label: 'Las Vegas, Nevada', + label: 'Las Vegas - Nevada', color: 'jade', position: 198, }, { key: 'lawrenceKansas', value: 'LAWRENCE_KANSAS', - label: 'Lawrence, Kansas', + label: 'Lawrence - Kansas', color: 'gray', position: 199, }, { key: 'lawtonOklahoma', value: 'LAWTON_OKLAHOMA', - label: 'Lawton, Oklahoma', + label: 'Lawton - Oklahoma', color: 'blue', position: 200, }, { key: 'lebanonPennsylvania', value: 'LEBANON_PENNSYLVANIA', - label: 'Lebanon, Pennsylvania', + label: 'Lebanon - Pennsylvania', color: 'red', position: 201, }, { key: 'lewistonIdaho', value: 'LEWISTON_IDAHO', - label: 'Lewiston, Idaho', + label: 'Lewiston - Idaho', color: 'green', position: 202, }, { key: 'lewistonMaine', value: 'LEWISTON_MAINE', - label: 'Lewiston, Maine', + label: 'Lewiston - Maine', color: 'orange', position: 203, }, { key: 'lexingtonKentucky', value: 'LEXINGTON_KENTUCKY', - label: 'Lexington, Kentucky', + label: 'Lexington - Kentucky', color: 'purple', position: 204, }, { key: 'limaOhio', value: 'LIMA_OHIO', - label: 'Lima, Ohio', + label: 'Lima - Ohio', color: 'yellow', position: 205, }, { key: 'lincolnNebraska', value: 'LINCOLN_NEBRASKA', - label: 'Lincoln, Nebraska', + label: 'Lincoln - Nebraska', color: 'pink', position: 206, }, { key: 'littleRockArkansas', value: 'LITTLE_ROCK_ARKANSAS', - label: 'Little Rock, Arkansas', + label: 'Little Rock - Arkansas', color: 'cyan', position: 207, }, { key: 'loganUtah', value: 'LOGAN_UTAH', - label: 'Logan, Utah', + label: 'Logan - Utah', color: 'brown', position: 208, }, { key: 'longviewTexas', value: 'LONGVIEW_TEXAS', - label: 'Longview, Texas', + label: 'Longview - Texas', color: 'lime', position: 209, }, { key: 'longviewWashington', value: 'LONGVIEW_WASHINGTON', - label: 'Longview, Washington', + label: 'Longview - Washington', color: 'violet', position: 210, }, { key: 'losAngelesCalifornia', value: 'LOS_ANGELES_CALIFORNIA', - label: 'Los Angeles, California', + label: 'Los Angeles - California', color: 'gold', position: 211, }, { key: 'louisvilleKentucky', value: 'LOUISVILLE_KENTUCKY', - label: 'Louisville, Kentucky', + label: 'Louisville - Kentucky', color: 'turquoise', position: 212, }, { key: 'lubbockTexas', value: 'LUBBOCK_TEXAS', - label: 'Lubbock, Texas', + label: 'Lubbock - Texas', color: 'crimson', position: 213, }, { key: 'lynchburgVirginia', value: 'LYNCHBURG_VIRGINIA', - label: 'Lynchburg, Virginia', + label: 'Lynchburg - Virginia', color: 'sky', position: 214, }, { key: 'maconGeorgia', value: 'MACON_GEORGIA', - label: 'Macon, Georgia', + label: 'Macon - Georgia', color: 'amber', position: 215, }, { key: 'maderaCalifornia', value: 'MADERA_CALIFORNIA', - label: 'Madera, California', + label: 'Madera - California', color: 'plum', position: 216, }, { key: 'madisonWisconsin', value: 'MADISON_WISCONSIN', - label: 'Madison, Wisconsin', + label: 'Madison - Wisconsin', color: 'grass', position: 217, }, { key: 'manchesterNewHampshire', value: 'MANCHESTER_NEW_HAMPSHIRE', - label: 'Manchester, New Hampshire', + label: 'Manchester - New Hampshire', color: 'tomato', position: 218, }, { key: 'manhattanKansas', value: 'MANHATTAN_KANSAS', - label: 'Manhattan, Kansas', + label: 'Manhattan - Kansas', color: 'iris', position: 219, }, { key: 'mankatoMinnesota', value: 'MANKATO_MINNESOTA', - label: 'Mankato, Minnesota', + label: 'Mankato - Minnesota', color: 'mint', position: 220, }, { key: 'mansfieldOhio', value: 'MANSFIELD_OHIO', - label: 'Mansfield, Ohio', + label: 'Mansfield - Ohio', color: 'ruby', position: 221, }, { key: 'mcallenTexas', value: 'MCALLEN_TEXAS', - label: 'McAllen, Texas', + label: 'McAllen - Texas', color: 'bronze', position: 222, }, { key: 'medfordOregon', value: 'MEDFORD_OREGON', - label: 'Medford, Oregon', + label: 'Medford - Oregon', color: 'jade', position: 223, }, { key: 'memphisTennessee', value: 'MEMPHIS_TENNESSEE', - label: 'Memphis, Tennessee', + label: 'Memphis - Tennessee', color: 'gray', position: 224, }, { key: 'mercedCalifornia', value: 'MERCED_CALIFORNIA', - label: 'Merced, California', + label: 'Merced - California', color: 'blue', position: 225, }, { key: 'miamiFlorida', value: 'MIAMI_FLORIDA', - label: 'Miami, Florida', + label: 'Miami - Florida', color: 'red', position: 226, }, { key: 'michiganCityIndiana', value: 'MICHIGAN_CITY_INDIANA', - label: 'Michigan City, Indiana', + label: 'Michigan City - Indiana', color: 'green', position: 227, }, { key: 'midlandMichigan', value: 'MIDLAND_MICHIGAN', - label: 'Midland, Michigan', + label: 'Midland - Michigan', color: 'orange', position: 228, }, { key: 'midlandTexas', value: 'MIDLAND_TEXAS', - label: 'Midland, Texas', + label: 'Midland - Texas', color: 'purple', position: 229, }, { key: 'milwaukeeWisconsin', value: 'MILWAUKEE_WISCONSIN', - label: 'Milwaukee, Wisconsin', + label: 'Milwaukee - Wisconsin', color: 'yellow', position: 230, }, { key: 'minneapolisMinnesota', value: 'MINNEAPOLIS_MINNESOTA', - label: 'Minneapolis, Minnesota', + label: 'Minneapolis - Minnesota', color: 'pink', position: 231, }, { key: 'missoulaMontana', value: 'MISSOULA_MONTANA', - label: 'Missoula, Montana', + label: 'Missoula - Montana', color: 'cyan', position: 232, }, { key: 'mobileAlabama', value: 'MOBILE_ALABAMA', - label: 'Mobile, Alabama', + label: 'Mobile - Alabama', color: 'brown', position: 233, }, { key: 'modestoCalifornia', value: 'MODESTO_CALIFORNIA', - label: 'Modesto, California', + label: 'Modesto - California', color: 'lime', position: 234, }, { key: 'monroeLouisiana', value: 'MONROE_LOUISIANA', - label: 'Monroe, Louisiana', + label: 'Monroe - Louisiana', color: 'violet', position: 235, }, { key: 'monroeMichigan', value: 'MONROE_MICHIGAN', - label: 'Monroe, Michigan', + label: 'Monroe - Michigan', color: 'gold', position: 236, }, { key: 'montgomeryAlabama', value: 'MONTGOMERY_ALABAMA', - label: 'Montgomery, Alabama', + label: 'Montgomery - Alabama', color: 'turquoise', position: 237, }, { key: 'morgantownWestVirginia', value: 'MORGANTOWN_WEST_VIRGINIA', - label: 'Morgantown, West Virginia', + label: 'Morgantown - West Virginia', color: 'crimson', position: 238, }, { key: 'morristownTennessee', value: 'MORRISTOWN_TENNESSEE', - label: 'Morristown, Tennessee', + label: 'Morristown - Tennessee', color: 'sky', position: 239, }, { key: 'mountVernonWashington', value: 'MOUNT_VERNON_WASHINGTON', - label: 'Mount Vernon, Washington', + label: 'Mount Vernon - Washington', color: 'amber', position: 240, }, { key: 'muncieIndiana', value: 'MUNCIE_INDIANA', - label: 'Muncie, Indiana', + label: 'Muncie - Indiana', color: 'plum', position: 241, }, { key: 'muskegonMichigan', value: 'MUSKEGON_MICHIGAN', - label: 'Muskegon, Michigan', + label: 'Muskegon - Michigan', color: 'grass', position: 242, }, { key: 'myrtleBeachSouthCarolina', value: 'MYRTLE_BEACH_SOUTH_CAROLINA', - label: 'Myrtle Beach, South Carolina', + label: 'Myrtle Beach - South Carolina', color: 'tomato', position: 243, }, { key: 'napaCalifornia', value: 'NAPA_CALIFORNIA', - label: 'Napa, California', + label: 'Napa - California', color: 'iris', position: 244, }, { key: 'naplesFlorida', value: 'NAPLES_FLORIDA', - label: 'Naples, Florida', + label: 'Naples - Florida', color: 'mint', position: 245, }, { key: 'nashvilleTennessee', value: 'NASHVILLE_TENNESSEE', - label: 'Nashville, Tennessee', + label: 'Nashville - Tennessee', color: 'ruby', position: 246, }, { key: 'newBernNorthCarolina', value: 'NEW_BERN_NORTH_CAROLINA', - label: 'New Bern, North Carolina', + label: 'New Bern - North Carolina', color: 'bronze', position: 247, }, { key: 'newHavenConnecticut', value: 'NEW_HAVEN_CONNECTICUT', - label: 'New Haven, Connecticut', + label: 'New Haven - Connecticut', color: 'jade', position: 248, }, { key: 'newOrleansLouisiana', value: 'NEW_ORLEANS_LOUISIANA', - label: 'New Orleans, Louisiana', + label: 'New Orleans - Louisiana', color: 'gray', position: 249, }, { key: 'newYorkNewYork', value: 'NEW_YORK_NEW_YORK', - label: 'New York, New York', + label: 'New York - New York', color: 'blue', position: 250, }, { key: 'nilesMichigan', value: 'NILES_MICHIGAN', - label: 'Niles, Michigan', + label: 'Niles - Michigan', color: 'red', position: 251, }, { key: 'northPortFlorida', value: 'NORTH_PORT_FLORIDA', - label: 'North Port, Florida', + label: 'North Port - Florida', color: 'green', position: 252, }, { key: 'norwichConnecticut', value: 'NORWICH_CONNECTICUT', - label: 'Norwich, Connecticut', + label: 'Norwich - Connecticut', color: 'orange', position: 253, }, { key: 'ocalaFlorida', value: 'OCALA_FLORIDA', - label: 'Ocala, Florida', + label: 'Ocala - Florida', color: 'purple', position: 254, }, { key: 'oceanCityNewJersey', value: 'OCEAN_CITY_NEW_JERSEY', - label: 'Ocean City, New Jersey', + label: 'Ocean City - New Jersey', color: 'yellow', position: 255, }, { key: 'odessaTexas', value: 'ODESSA_TEXAS', - label: 'Odessa, Texas', + label: 'Odessa - Texas', color: 'pink', position: 256, }, { key: 'ogdenUtah', value: 'OGDEN_UTAH', - label: 'Ogden, Utah', + label: 'Ogden - Utah', color: 'cyan', position: 257, }, { key: 'oklahomaCityOklahoma', value: 'OKLAHOMA_CITY_OKLAHOMA', - label: 'Oklahoma City, Oklahoma', + label: 'Oklahoma City - Oklahoma', color: 'brown', position: 258, }, { key: 'olympiaWashington', value: 'OLYMPIA_WASHINGTON', - label: 'Olympia, Washington', + label: 'Olympia - Washington', color: 'lime', position: 259, }, { key: 'omahaNebraska', value: 'OMAHA_NEBRASKA', - label: 'Omaha, Nebraska', + label: 'Omaha - Nebraska', color: 'violet', position: 260, }, { key: 'orlandoFlorida', value: 'ORLANDO_FLORIDA', - label: 'Orlando, Florida', + label: 'Orlando - Florida', color: 'gold', position: 261, }, { key: 'oshkoshWisconsin', value: 'OSHKOSH_WISCONSIN', - label: 'Oshkosh, Wisconsin', + label: 'Oshkosh - Wisconsin', color: 'turquoise', position: 262, }, { key: 'owensboroKentucky', value: 'OWENSBORO_KENTUCKY', - label: 'Owensboro, Kentucky', + label: 'Owensboro - Kentucky', color: 'crimson', position: 263, }, { key: 'oxnardCalifornia', value: 'OXNARD_CALIFORNIA', - label: 'Oxnard, California', + label: 'Oxnard - California', color: 'sky', position: 264, }, { key: 'palmBayFlorida', value: 'PALM_BAY_FLORIDA', - label: 'Palm Bay, Florida', + label: 'Palm Bay - Florida', color: 'amber', position: 265, }, { key: 'panamaCityFlorida', value: 'PANAMA_CITY_FLORIDA', - label: 'Panama City, Florida', + label: 'Panama City - Florida', color: 'plum', position: 266, }, { key: 'parkersburgWestVirginia', value: 'PARKERSBURG_WEST_VIRGINIA', - label: 'Parkersburg, West Virginia', + label: 'Parkersburg - West Virginia', color: 'grass', position: 267, }, { key: 'pensacolaFlorida', value: 'PENSACOLA_FLORIDA', - label: 'Pensacola, Florida', + label: 'Pensacola - Florida', color: 'tomato', position: 268, }, { key: 'peoriaIllinois', value: 'PEORIA_ILLINOIS', - label: 'Peoria, Illinois', + label: 'Peoria - Illinois', color: 'iris', position: 269, }, { key: 'philadelphiaPennsylvania', value: 'PHILADELPHIA_PENNSYLVANIA', - label: 'Philadelphia, Pennsylvania', + label: 'Philadelphia - Pennsylvania', color: 'mint', position: 270, }, { key: 'phoenixArizona', value: 'PHOENIX_ARIZONA', - label: 'Phoenix, Arizona', + label: 'Phoenix - Arizona', color: 'ruby', position: 271, }, { key: 'pineBluffArkansas', value: 'PINE_BLUFF_ARKANSAS', - label: 'Pine Bluff, Arkansas', + label: 'Pine Bluff - Arkansas', color: 'bronze', position: 272, }, { key: 'pittsburghPennsylvania', value: 'PITTSBURGH_PENNSYLVANIA', - label: 'Pittsburgh, Pennsylvania', + label: 'Pittsburgh - Pennsylvania', color: 'jade', position: 273, }, { key: 'pittsfieldMassachusetts', value: 'PITTSFIELD_MASSACHUSETTS', - label: 'Pittsfield, Massachusetts', + label: 'Pittsfield - Massachusetts', color: 'gray', position: 274, }, { key: 'pocatelloIdaho', value: 'POCATELLO_IDAHO', - label: 'Pocatello, Idaho', + label: 'Pocatello - Idaho', color: 'blue', position: 275, }, { key: 'portStLucieFlorida', value: 'PORT_ST_LUCIE_FLORIDA', - label: 'Port St. Lucie, Florida', + label: 'Port St. Lucie - Florida', color: 'red', position: 276, }, { key: 'portlandMaine', value: 'PORTLAND_MAINE', - label: 'Portland, Maine', + label: 'Portland - Maine', color: 'green', position: 277, }, { key: 'portlandOregon', value: 'PORTLAND_OREGON', - label: 'Portland, Oregon', + label: 'Portland - Oregon', color: 'orange', position: 278, }, { key: 'poughkeepsieNewYork', value: 'POUGHKEEPSIE_NEW_YORK', - label: 'Poughkeepsie, New York', + label: 'Poughkeepsie - New York', color: 'purple', position: 279, }, { key: 'prescottValleyArizona', value: 'PRESCOTT_VALLEY_ARIZONA', - label: 'Prescott Valley, Arizona', + label: 'Prescott Valley - Arizona', color: 'yellow', position: 280, }, { key: 'providenceRhodeIsland', value: 'PROVIDENCE_RHODE_ISLAND', - label: 'Providence, Rhode Island', + label: 'Providence - Rhode Island', color: 'pink', position: 281, }, { key: 'provoUtah', value: 'PROVO_UTAH', - label: 'Provo, Utah', + label: 'Provo - Utah', color: 'cyan', position: 282, }, { key: 'puebloColorado', value: 'PUEBLO_COLORADO', - label: 'Pueblo, Colorado', + label: 'Pueblo - Colorado', color: 'brown', position: 283, }, { key: 'puntaGordaFlorida', value: 'PUNTA_GORDA_FLORIDA', - label: 'Punta Gorda, Florida', + label: 'Punta Gorda - Florida', color: 'lime', position: 284, }, { key: 'racineWisconsin', value: 'RACINE_WISCONSIN', - label: 'Racine, Wisconsin', + label: 'Racine - Wisconsin', color: 'violet', position: 285, }, { key: 'raleighNorthCarolina', value: 'RALEIGH_NORTH_CAROLINA', - label: 'Raleigh, North Carolina', + label: 'Raleigh - North Carolina', color: 'gold', position: 286, }, { key: 'rapidCitySouthDakota', value: 'RAPID_CITY_SOUTH_DAKOTA', - label: 'Rapid City, South Dakota', + label: 'Rapid City - South Dakota', color: 'turquoise', position: 287, }, { key: 'readingPennsylvania', value: 'READING_PENNSYLVANIA', - label: 'Reading, Pennsylvania', + label: 'Reading - Pennsylvania', color: 'crimson', position: 288, }, { key: 'reddingCalifornia', value: 'REDDING_CALIFORNIA', - label: 'Redding, California', + label: 'Redding - California', color: 'sky', position: 289, }, { key: 'renoNevada', value: 'RENO_NEVADA', - label: 'Reno, Nevada', + label: 'Reno - Nevada', color: 'amber', position: 290, }, { key: 'richmondVirginia', value: 'RICHMOND_VIRGINIA', - label: 'Richmond, Virginia', + label: 'Richmond - Virginia', color: 'plum', position: 291, }, { key: 'riversideCalifornia', value: 'RIVERSIDE_CALIFORNIA', - label: 'Riverside, California', + label: 'Riverside - California', color: 'grass', position: 292, }, { key: 'roanokeVirginia', value: 'ROANOKE_VIRGINIA', - label: 'Roanoke, Virginia', + label: 'Roanoke - Virginia', color: 'tomato', position: 293, }, { key: 'rochesterMinnesota', value: 'ROCHESTER_MINNESOTA', - label: 'Rochester, Minnesota', + label: 'Rochester - Minnesota', color: 'iris', position: 294, }, { key: 'rochesterNewYork', value: 'ROCHESTER_NEW_YORK', - label: 'Rochester, New York', + label: 'Rochester - New York', color: 'mint', position: 295, }, { key: 'rockfordIllinois', value: 'ROCKFORD_ILLINOIS', - label: 'Rockford, Illinois', + label: 'Rockford - Illinois', color: 'ruby', position: 296, }, { key: 'rockyMountNorthCarolina', value: 'ROCKY_MOUNT_NORTH_CAROLINA', - label: 'Rocky Mount, North Carolina', + label: 'Rocky Mount - North Carolina', color: 'bronze', position: 297, }, { key: 'romeGeorgia', value: 'ROME_GEORGIA', - label: 'Rome, Georgia', + label: 'Rome - Georgia', color: 'jade', position: 298, }, { key: 'sacramentoCalifornia', value: 'SACRAMENTO_CALIFORNIA', - label: 'Sacramento, California', + label: 'Sacramento - California', color: 'gray', position: 299, }, { key: 'saginawMichigan', value: 'SAGINAW_MICHIGAN', - label: 'Saginaw, Michigan', + label: 'Saginaw - Michigan', color: 'blue', position: 300, }, { key: 'salemOregon', value: 'SALEM_OREGON', - label: 'Salem, Oregon', + label: 'Salem - Oregon', color: 'red', position: 301, }, { key: 'salinasCalifornia', value: 'SALINAS_CALIFORNIA', - label: 'Salinas, California', + label: 'Salinas - California', color: 'green', position: 302, }, { key: 'salisburyMaryland', value: 'SALISBURY_MARYLAND', - label: 'Salisbury, Maryland', + label: 'Salisbury - Maryland', color: 'orange', position: 303, }, { key: 'saltLakeCityUtah', value: 'SALT_LAKE_CITY_UTAH', - label: 'Salt Lake City, Utah', + label: 'Salt Lake City - Utah', color: 'purple', position: 304, }, { key: 'sanAngeloTexas', value: 'SAN_ANGELO_TEXAS', - label: 'San Angelo, Texas', + label: 'San Angelo - Texas', color: 'yellow', position: 305, }, { key: 'sanAntonioTexas', value: 'SAN_ANTONIO_TEXAS', - label: 'San Antonio, Texas', + label: 'San Antonio - Texas', color: 'pink', position: 306, }, { key: 'sanDiegoCalifornia', value: 'SAN_DIEGO_CALIFORNIA', - label: 'San Diego, California', + label: 'San Diego - California', color: 'cyan', position: 307, }, { key: 'sanFranciscoCalifornia', value: 'SAN_FRANCISCO_CALIFORNIA', - label: 'San Francisco, California', + label: 'San Francisco - California', color: 'brown', position: 308, }, { key: 'sanJoseCalifornia', value: 'SAN_JOSE_CALIFORNIA', - label: 'San Jose, California', + label: 'San Jose - California', color: 'lime', position: 309, }, { key: 'sanLuisObispoCalifornia', value: 'SAN_LUIS_OBISPO_CALIFORNIA', - label: 'San Luis Obispo, California', + label: 'San Luis Obispo - California', color: 'violet', position: 310, }, { key: 'santaCruzCalifornia', value: 'SANTA_CRUZ_CALIFORNIA', - label: 'Santa Cruz, California', + label: 'Santa Cruz - California', color: 'gold', position: 311, }, { key: 'santaFeNewMexico', value: 'SANTA_FE_NEW_MEXICO', - label: 'Santa Fe, New Mexico', + label: 'Santa Fe - New Mexico', color: 'turquoise', position: 312, }, { key: 'santaMariaCalifornia', value: 'SANTA_MARIA_CALIFORNIA', - label: 'Santa Maria, California', + label: 'Santa Maria - California', color: 'crimson', position: 313, }, { key: 'santaRosaCalifornia', value: 'SANTA_ROSA_CALIFORNIA', - label: 'Santa Rosa, California', + label: 'Santa Rosa - California', color: 'sky', position: 314, }, { key: 'savannahGeorgia', value: 'SAVANNAH_GEORGIA', - label: 'Savannah, Georgia', + label: 'Savannah - Georgia', color: 'amber', position: 315, }, { key: 'scrantonPennsylvania', value: 'SCRANTON_PENNSYLVANIA', - label: 'Scranton, Pennsylvania', + label: 'Scranton - Pennsylvania', color: 'plum', position: 316, }, { key: 'seattleWashington', value: 'SEATTLE_WASHINGTON', - label: 'Seattle, Washington', + label: 'Seattle - Washington', color: 'grass', position: 317, }, { key: 'sebastianFlorida', value: 'SEBASTIAN_FLORIDA', - label: 'Sebastian, Florida', + label: 'Sebastian - Florida', color: 'tomato', position: 318, }, { key: 'sebringFlorida', value: 'SEBRING_FLORIDA', - label: 'Sebring, Florida', + label: 'Sebring - Florida', color: 'iris', position: 319, }, { key: 'sheboyganWisconsin', value: 'SHEBOYGAN_WISCONSIN', - label: 'Sheboygan, Wisconsin', + label: 'Sheboygan - Wisconsin', color: 'mint', position: 320, }, { key: 'shermanTexas', value: 'SHERMAN_TEXAS', - label: 'Sherman, Texas', + label: 'Sherman - Texas', color: 'ruby', position: 321, }, { key: 'shreveportLouisiana', value: 'SHREVEPORT_LOUISIANA', - label: 'Shreveport, Louisiana', + label: 'Shreveport - Louisiana', color: 'bronze', position: 322, }, { key: 'sierraVistaArizona', value: 'SIERRA_VISTA_ARIZONA', - label: 'Sierra Vista, Arizona', + label: 'Sierra Vista - Arizona', color: 'jade', position: 323, }, { key: 'siouxCityIowa', value: 'SIOUX_CITY_IOWA', - label: 'Sioux City, Iowa', + label: 'Sioux City - Iowa', color: 'gray', position: 324, }, { key: 'siouxFallsSouthDakota', value: 'SIOUX_FALLS_SOUTH_DAKOTA', - label: 'Sioux Falls, South Dakota', + label: 'Sioux Falls - South Dakota', color: 'blue', position: 325, }, { key: 'southBendIndiana', value: 'SOUTH_BEND_INDIANA', - label: 'South Bend, Indiana', + label: 'South Bend - Indiana', color: 'red', position: 326, }, { key: 'spartanburgSouthCarolina', value: 'SPARTANBURG_SOUTH_CAROLINA', - label: 'Spartanburg, South Carolina', + label: 'Spartanburg - South Carolina', color: 'green', position: 327, }, { key: 'spokaneWashington', value: 'SPOKANE_WASHINGTON', - label: 'Spokane, Washington', + label: 'Spokane - Washington', color: 'orange', position: 328, }, { key: 'springfieldIllinois', value: 'SPRINGFIELD_ILLINOIS', - label: 'Springfield, Illinois', + label: 'Springfield - Illinois', color: 'purple', position: 329, }, { key: 'springfieldMassachusetts', value: 'SPRINGFIELD_MASSACHUSETTS', - label: 'Springfield, Massachusetts', + label: 'Springfield - Massachusetts', color: 'yellow', position: 330, }, { key: 'springfieldMissouri', value: 'SPRINGFIELD_MISSOURI', - label: 'Springfield, Missouri', + label: 'Springfield - Missouri', color: 'pink', position: 331, }, { key: 'springfieldOhio', value: 'SPRINGFIELD_OHIO', - label: 'Springfield, Ohio', + label: 'Springfield - Ohio', color: 'cyan', position: 332, }, { key: 'stCloudMinnesota', value: 'ST_CLOUD_MINNESOTA', - label: 'St. Cloud, Minnesota', + label: 'St. Cloud - Minnesota', color: 'brown', position: 333, }, { key: 'stGeorgeUtah', value: 'ST_GEORGE_UTAH', - label: 'St. George, Utah', + label: 'St. George - Utah', color: 'lime', position: 334, }, { key: 'stJosephMissouri', value: 'ST_JOSEPH_MISSOURI', - label: 'St. Joseph, Missouri', + label: 'St. Joseph - Missouri', color: 'violet', position: 335, }, { key: 'stLouisMissouri', value: 'ST_LOUIS_MISSOURI', - label: 'St. Louis, Missouri', + label: 'St. Louis - Missouri', color: 'gold', position: 336, }, { key: 'stateCollegePennsylvania', value: 'STATE_COLLEGE_PENNSYLVANIA', - label: 'State College, Pennsylvania', + label: 'State College - Pennsylvania', color: 'turquoise', position: 337, }, { key: 'stauntonVirginia', value: 'STAUNTON_VIRGINIA', - label: 'Staunton, Virginia', + label: 'Staunton - Virginia', color: 'crimson', position: 338, }, { key: 'stocktonCalifornia', value: 'STOCKTON_CALIFORNIA', - label: 'Stockton, California', + label: 'Stockton - California', color: 'sky', position: 339, }, { key: 'sumterSouthCarolina', value: 'SUMTER_SOUTH_CAROLINA', - label: 'Sumter, South Carolina', + label: 'Sumter - South Carolina', color: 'amber', position: 340, }, { key: 'syracuseNewYork', value: 'SYRACUSE_NEW_YORK', - label: 'Syracuse, New York', + label: 'Syracuse - New York', color: 'plum', position: 341, }, { key: 'tallahasseeFlorida', value: 'TALLAHASSEE_FLORIDA', - label: 'Tallahassee, Florida', + label: 'Tallahassee - Florida', color: 'grass', position: 342, }, { key: 'tampaFlorida', value: 'TAMPA_FLORIDA', - label: 'Tampa, Florida', + label: 'Tampa - Florida', color: 'tomato', position: 343, }, { key: 'terreHauteIndiana', value: 'TERRE_HAUTE_INDIANA', - label: 'Terre Haute, Indiana', + label: 'Terre Haute - Indiana', color: 'iris', position: 344, }, { key: 'texarkanaTexas', value: 'TEXARKANA_TEXAS', - label: 'Texarkana, Texas', + label: 'Texarkana - Texas', color: 'mint', position: 345, }, { key: 'theVillagesFlorida', value: 'THE_VILLAGES_FLORIDA', - label: 'The Villages, Florida', + label: 'The Villages - Florida', color: 'ruby', position: 346, }, { key: 'toledoOhio', value: 'TOLEDO_OHIO', - label: 'Toledo, Ohio', + label: 'Toledo - Ohio', color: 'bronze', position: 347, }, { key: 'topekaKansas', value: 'TOPEKA_KANSAS', - label: 'Topeka, Kansas', + label: 'Topeka - Kansas', color: 'jade', position: 348, }, { key: 'trentonNewJersey', value: 'TRENTON_NEW_JERSEY', - label: 'Trenton, New Jersey', + label: 'Trenton - New Jersey', color: 'gray', position: 349, }, { key: 'tucsonArizona', value: 'TUCSON_ARIZONA', - label: 'Tucson, Arizona', + label: 'Tucson - Arizona', color: 'blue', position: 350, }, { key: 'tulsaOklahoma', value: 'TULSA_OKLAHOMA', - label: 'Tulsa, Oklahoma', + label: 'Tulsa - Oklahoma', color: 'red', position: 351, }, { key: 'tuscaloosaAlabama', value: 'TUSCALOOSA_ALABAMA', - label: 'Tuscaloosa, Alabama', + label: 'Tuscaloosa - Alabama', color: 'green', position: 352, }, { key: 'twinFallsIdaho', value: 'TWIN_FALLS_IDAHO', - label: 'Twin Falls, Idaho', + label: 'Twin Falls - Idaho', color: 'orange', position: 353, }, { key: 'tylerTexas', value: 'TYLER_TEXAS', - label: 'Tyler, Texas', + label: 'Tyler - Texas', color: 'purple', position: 354, }, { key: 'urbanHonoluluHawaii', value: 'URBAN_HONOLULU_HAWAII', - label: 'Urban Honolulu, Hawaii', + label: 'Urban Honolulu - Hawaii', color: 'yellow', position: 355, }, { key: 'uticaNewYork', value: 'UTICA_NEW_YORK', - label: 'Utica, New York', + label: 'Utica - New York', color: 'pink', position: 356, }, { key: 'valdostaGeorgia', value: 'VALDOSTA_GEORGIA', - label: 'Valdosta, Georgia', + label: 'Valdosta - Georgia', color: 'cyan', position: 357, }, { key: 'vallejoCalifornia', value: 'VALLEJO_CALIFORNIA', - label: 'Vallejo, California', + label: 'Vallejo - California', color: 'brown', position: 358, }, { key: 'victoriaTexas', value: 'VICTORIA_TEXAS', - label: 'Victoria, Texas', + label: 'Victoria - Texas', color: 'lime', position: 359, }, { key: 'vinelandNewJersey', value: 'VINELAND_NEW_JERSEY', - label: 'Vineland, New Jersey', + label: 'Vineland - New Jersey', color: 'violet', position: 360, }, { key: 'virginiaBeachVirginia', value: 'VIRGINIA_BEACH_VIRGINIA', - label: 'Virginia Beach, Virginia', + label: 'Virginia Beach - Virginia', color: 'gold', position: 361, }, { key: 'visaliaCalifornia', value: 'VISALIA_CALIFORNIA', - label: 'Visalia, California', + label: 'Visalia - California', color: 'turquoise', position: 362, }, { key: 'wacoTexas', value: 'WACO_TEXAS', - label: 'Waco, Texas', + label: 'Waco - Texas', color: 'crimson', position: 363, }, { key: 'wallaWallaWashington', value: 'WALLA_WALLA_WASHINGTON', - label: 'Walla Walla, Washington', + label: 'Walla Walla - Washington', color: 'sky', position: 364, }, { key: 'warnerRobinsGeorgia', value: 'WARNER_ROBINS_GEORGIA', - label: 'Warner Robins, Georgia', + label: 'Warner Robins - Georgia', color: 'amber', position: 365, }, { key: 'waterlooIowa', value: 'WATERLOO_IOWA', - label: 'Waterloo, Iowa', + label: 'Waterloo - Iowa', color: 'plum', position: 366, }, { key: 'watertownNewYork', value: 'WATERTOWN_NEW_YORK', - label: 'Watertown, New York', + label: 'Watertown - New York', color: 'grass', position: 367, }, { key: 'wausauWisconsin', value: 'WAUSAU_WISCONSIN', - label: 'Wausau, Wisconsin', + label: 'Wausau - Wisconsin', color: 'tomato', position: 368, }, { key: 'weirtonWestVirginia', value: 'WEIRTON_WEST_VIRGINIA', - label: 'Weirton, West Virginia', + label: 'Weirton - West Virginia', color: 'iris', position: 369, }, { key: 'wenatcheeWashington', value: 'WENATCHEE_WASHINGTON', - label: 'Wenatchee, Washington', + label: 'Wenatchee - Washington', color: 'mint', position: 370, }, { key: 'wheelingWestVirginia', value: 'WHEELING_WEST_VIRGINIA', - label: 'Wheeling, West Virginia', + label: 'Wheeling - West Virginia', color: 'ruby', position: 371, }, { key: 'wichitaFallsTexas', value: 'WICHITA_FALLS_TEXAS', - label: 'Wichita Falls, Texas', + label: 'Wichita Falls - Texas', color: 'bronze', position: 372, }, { key: 'wichitaKansas', value: 'WICHITA_KANSAS', - label: 'Wichita, Kansas', + label: 'Wichita - Kansas', color: 'jade', position: 373, }, { key: 'williamsportPennsylvania', value: 'WILLIAMSPORT_PENNSYLVANIA', - label: 'Williamsport, Pennsylvania', + label: 'Williamsport - Pennsylvania', color: 'gray', position: 374, }, { key: 'wilmingtonNorthCarolina', value: 'WILMINGTON_NORTH_CAROLINA', - label: 'Wilmington, North Carolina', + label: 'Wilmington - North Carolina', color: 'blue', position: 375, }, { key: 'winchesterVirginia', value: 'WINCHESTER_VIRGINIA', - label: 'Winchester, Virginia', + label: 'Winchester - Virginia', color: 'red', position: 376, }, { key: 'winstonNorthCarolina', value: 'WINSTON_NORTH_CAROLINA', - label: 'Winston, North Carolina', + label: 'Winston - North Carolina', color: 'green', position: 377, }, { key: 'worcesterMassachusetts', value: 'WORCESTER_MASSACHUSETTS', - label: 'Worcester, Massachusetts', + label: 'Worcester - Massachusetts', color: 'orange', position: 378, }, { key: 'yakimaWashington', value: 'YAKIMA_WASHINGTON', - label: 'Yakima, Washington', + label: 'Yakima - Washington', color: 'purple', position: 379, }, { key: 'yorkPennsylvania', value: 'YORK_PENNSYLVANIA', - label: 'York, Pennsylvania', + label: 'York - Pennsylvania', color: 'yellow', position: 380, }, { key: 'youngstownOhio', value: 'YOUNGSTOWN_OHIO', - label: 'Youngstown, Ohio', + label: 'Youngstown - Ohio', color: 'pink', position: 381, }, { key: 'yubaCityCalifornia', value: 'YUBA_CITY_CALIFORNIA', - label: 'Yuba City, California', + label: 'Yuba City - California', color: 'cyan', position: 382, }, { key: 'yumaArizona', value: 'YUMA_ARIZONA', - label: 'Yuma, Arizona', + label: 'Yuma - Arizona', color: 'brown', position: 383, }, diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/mic-exchange-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/mic-exchange-options.ts new file mode 100644 index 0000000000000..f8649718c46f4 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/mic-exchange-options.ts @@ -0,0 +1,494 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const MIC_EXCHANGE_OPTIONS: readonly SelectOptionMeta[] = [ + { + key: 'aqse', + value: 'AQSE', + label: 'AQSE', + color: 'blue', + position: 0, + }, + { + key: 'arcx', + value: 'ARCX', + label: 'ARCX', + color: 'red', + position: 1, + }, + { + key: 'asex', + value: 'ASEX', + label: 'ASEX', + color: 'green', + position: 2, + }, + { + key: 'bcxe', + value: 'BCXE', + label: 'BCXE', + color: 'orange', + position: 3, + }, + { + key: 'bvmf', + value: 'BVMF', + label: 'BVMF', + color: 'purple', + position: 4, + }, + { + key: 'dsmd', + value: 'DSMD', + label: 'DSMD', + color: 'yellow', + position: 5, + }, + { + key: 'misx', + value: 'MISX', + label: 'MISX', + color: 'pink', + position: 6, + }, + { + key: 'neoe', + value: 'NEOE', + label: 'NEOE', + color: 'cyan', + position: 7, + }, + { + key: 'xams', + value: 'XAMS', + label: 'XAMS', + color: 'brown', + position: 8, + }, + { + key: 'xase', + value: 'XASE', + label: 'XASE', + color: 'lime', + position: 9, + }, + { + key: 'xasx', + value: 'XASX', + label: 'XASX', + color: 'violet', + position: 10, + }, + { + key: 'xber', + value: 'XBER', + label: 'XBER', + color: 'gold', + position: 11, + }, + { + key: 'xbkk', + value: 'XBKK', + label: 'XBKK', + color: 'turquoise', + position: 12, + }, + { + key: 'xbog', + value: 'XBOG', + label: 'XBOG', + color: 'crimson', + position: 13, + }, + { + key: 'xbom', + value: 'XBOM', + label: 'XBOM', + color: 'sky', + position: 14, + }, + { + key: 'xbru', + value: 'XBRU', + label: 'XBRU', + color: 'amber', + position: 15, + }, + { + key: 'xbsp', + value: 'XBSP', + label: 'XBSP', + color: 'plum', + position: 16, + }, + { + key: 'xbud', + value: 'XBUD', + label: 'XBUD', + color: 'grass', + position: 17, + }, + { + key: 'xbue', + value: 'XBUE', + label: 'XBUE', + color: 'tomato', + position: 18, + }, + { + key: 'xcai', + value: 'XCAI', + label: 'XCAI', + color: 'iris', + position: 19, + }, + { + key: 'xcbo', + value: 'XCBO', + label: 'XCBO', + color: 'mint', + position: 20, + }, + { + key: 'xcnq', + value: 'XCNQ', + label: 'XCNQ', + color: 'ruby', + position: 21, + }, + { + key: 'xcse', + value: 'XCSE', + label: 'XCSE', + color: 'bronze', + position: 22, + }, + { + key: 'xdfm', + value: 'XDFM', + label: 'XDFM', + color: 'jade', + position: 23, + }, + { + key: 'xdub', + value: 'XDUB', + label: 'XDUB', + color: 'gray', + position: 24, + }, + { + key: 'xdus', + value: 'XDUS', + label: 'XDUS', + color: 'blue', + position: 25, + }, + { + key: 'xeqy', + value: 'XEQY', + label: 'XEQY', + color: 'red', + position: 26, + }, + { + key: 'xfra', + value: 'XFRA', + label: 'XFRA', + color: 'green', + position: 27, + }, + { + key: 'xham', + value: 'XHAM', + label: 'XHAM', + color: 'orange', + position: 28, + }, + { + key: 'xhel', + value: 'XHEL', + label: 'XHEL', + color: 'purple', + position: 29, + }, + { + key: 'xhkg', + value: 'XHKG', + label: 'XHKG', + color: 'yellow', + position: 30, + }, + { + key: 'xice', + value: 'XICE', + label: 'XICE', + color: 'pink', + position: 31, + }, + { + key: 'xidx', + value: 'XIDX', + label: 'XIDX', + color: 'cyan', + position: 32, + }, + { + key: 'xjpx', + value: 'XJPX', + label: 'XJPX', + color: 'brown', + position: 33, + }, + { + key: 'xjse', + value: 'XJSE', + label: 'XJSE', + color: 'lime', + position: 34, + }, + { + key: 'xkls', + value: 'XKLS', + label: 'XKLS', + color: 'violet', + position: 35, + }, + { + key: 'xkos', + value: 'XKOS', + label: 'XKOS', + color: 'gold', + position: 36, + }, + { + key: 'xkrx', + value: 'XKRX', + label: 'XKRX', + color: 'turquoise', + position: 37, + }, + { + key: 'xkuw', + value: 'XKUW', + label: 'XKUW', + color: 'crimson', + position: 38, + }, + { + key: 'xlis', + value: 'XLIS', + label: 'XLIS', + color: 'sky', + position: 39, + }, + { + key: 'xlon', + value: 'XLON', + label: 'XLON', + color: 'amber', + position: 40, + }, + { + key: 'xmad', + value: 'XMAD', + label: 'XMAD', + color: 'plum', + position: 41, + }, + { + key: 'xmex', + value: 'XMEX', + label: 'XMEX', + color: 'grass', + position: 42, + }, + { + key: 'xmil', + value: 'XMIL', + label: 'XMIL', + color: 'tomato', + position: 43, + }, + { + key: 'xmun', + value: 'XMUN', + label: 'XMUN', + color: 'iris', + position: 44, + }, + { + key: 'xnas', + value: 'XNAS', + label: 'XNAS', + color: 'mint', + position: 45, + }, + { + key: 'xnse', + value: 'XNSE', + label: 'XNSE', + color: 'ruby', + position: 46, + }, + { + key: 'xnys', + value: 'XNYS', + label: 'XNYS', + color: 'bronze', + position: 47, + }, + { + key: 'xnze', + value: 'XNZE', + label: 'XNZE', + color: 'jade', + position: 48, + }, + { + key: 'xosl', + value: 'XOSL', + label: 'XOSL', + color: 'gray', + position: 49, + }, + { + key: 'xotc', + value: 'XOTC', + label: 'XOTC', + color: 'blue', + position: 50, + }, + { + key: 'xpar', + value: 'XPAR', + label: 'XPAR', + color: 'red', + position: 51, + }, + { + key: 'xpra', + value: 'XPRA', + label: 'XPRA', + color: 'green', + position: 52, + }, + { + key: 'xris', + value: 'XRIS', + label: 'XRIS', + color: 'orange', + position: 53, + }, + { + key: 'xsau', + value: 'XSAU', + label: 'XSAU', + color: 'purple', + position: 54, + }, + { + key: 'xses', + value: 'XSES', + label: 'XSES', + color: 'yellow', + position: 55, + }, + { + key: 'xsgo', + value: 'XSGO', + label: 'XSGO', + color: 'pink', + position: 56, + }, + { + key: 'xshe', + value: 'XSHE', + label: 'XSHE', + color: 'cyan', + position: 57, + }, + { + key: 'xshg', + value: 'XSHG', + label: 'XSHG', + color: 'brown', + position: 58, + }, + { + key: 'xstc', + value: 'XSTC', + label: 'XSTC', + color: 'lime', + position: 59, + }, + { + key: 'xsto', + value: 'XSTO', + label: 'XSTO', + color: 'violet', + position: 60, + }, + { + key: 'xstu', + value: 'XSTU', + label: 'XSTU', + color: 'gold', + position: 61, + }, + { + key: 'xswx', + value: 'XSWX', + label: 'XSWX', + color: 'turquoise', + position: 62, + }, + { + key: 'xtae', + value: 'XTAE', + label: 'XTAE', + color: 'crimson', + position: 63, + }, + { + key: 'xtai', + value: 'XTAI', + label: 'XTAI', + color: 'sky', + position: 64, + }, + { + key: 'xtal', + value: 'XTAL', + label: 'XTAL', + color: 'amber', + position: 65, + }, + { + key: 'xtse', + value: 'XTSE', + label: 'XTSE', + color: 'plum', + position: 66, + }, + { + key: 'xtsx', + value: 'XTSX', + label: 'XTSX', + color: 'grass', + position: 67, + }, + { + key: 'xwar', + value: 'XWAR', + label: 'XWAR', + color: 'tomato', + position: 68, + }, + { + key: 'xwbo', + value: 'XWBO', + label: 'XWBO', + color: 'iris', + position: 69, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/seniority-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/seniority-options.ts new file mode 100644 index 0000000000000..89e6f2bc85ae7 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/seniority-options.ts @@ -0,0 +1,74 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const SENIORITY_OPTIONS: readonly SelectOptionMeta[] = [ + { + key: 'cxo', + value: 'CXO', + label: 'CXO', + color: 'blue', + position: 0, + }, + { + key: 'owner', + value: 'OWNER', + label: 'Owner', + color: 'red', + position: 1, + }, + { + key: 'vp', + value: 'VP', + label: 'VP', + color: 'green', + position: 2, + }, + { + key: 'director', + value: 'DIRECTOR', + label: 'Director', + color: 'orange', + position: 3, + }, + { + key: 'partner', + value: 'PARTNER', + label: 'Partner', + color: 'purple', + position: 4, + }, + { + key: 'senior', + value: 'SENIOR', + label: 'Senior', + color: 'yellow', + position: 5, + }, + { + key: 'manager', + value: 'MANAGER', + label: 'Manager', + color: 'pink', + position: 6, + }, + { + key: 'entry', + value: 'ENTRY', + label: 'Entry', + color: 'cyan', + position: 7, + }, + { + key: 'training', + value: 'TRAINING', + label: 'Training', + color: 'brown', + position: 8, + }, + { + key: 'unpaid', + value: 'UNPAID', + label: 'Unpaid', + color: 'lime', + position: 9, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/sex-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/sex-options.ts new file mode 100644 index 0000000000000..7c69856e3c050 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/sex-options.ts @@ -0,0 +1,18 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const SEX_OPTIONS: readonly SelectOptionMeta[] = [ + { + key: 'male', + value: 'MALE', + label: 'Male', + color: 'blue', + position: 0, + }, + { + key: 'female', + value: 'FEMALE', + label: 'Female', + color: 'red', + position: 1, + }, +]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/size-options.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/size-options.ts index 6364ab6124b8e..cd054e7f8d77b 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/constants/size-options.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/size-options.ts @@ -1,4 +1,4 @@ -import { type SelectOptionMeta } from 'src/types/select-option-meta.type'; +import { type SelectOptionMeta } from 'src/types/select-option-meta'; export const SIZE_OPTIONS: readonly SelectOptionMeta[] = [ { diff --git a/packages/twenty-apps/internal/people-data-labs/src/constants/universal-identifiers.ts b/packages/twenty-apps/internal/people-data-labs/src/constants/universal-identifiers.ts index f615865866e5c..2635aeeed400b 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/constants/universal-identifiers.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/constants/universal-identifiers.ts @@ -4,6 +4,14 @@ export const APPLICATION_UNIVERSAL_IDENTIFIER = export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'abb2aa9f-8e9c-4e8b-a336-f864ee78b7cd'; +export const PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS = { + enrichPeople: '65356a82-6734-4fc9-8172-7d30ed1b7859', + enrichPersonTool: 'c1539ca9-6f57-4036-a2a7-621ec23a66e6', + enrichCompanies: 'c769fb49-d495-469f-a58f-1a69ab90ec24', + enrichCompanyTool: '88d126e1-a8f4-49f2-883f-39a7fa69cede', + postInstall: '9de46f15-05ec-4314-84c1-b9919b545269', +} as const; + export const PDL_FIELD_UNIVERSAL_IDENTIFIERS = { person: { pdlId: 'f6c45913-4ad7-4889-8b70-6e49fbae72cf', @@ -21,7 +29,6 @@ export const PDL_FIELD_UNIVERSAL_IDENTIFIERS = { pdlRawPayload: '91153108-0576-4da8-a58d-bf8bfc693af1', pdlLastEnrichedAt: '437e08e7-4a94-45af-9def-f202d54ea910', pdlLocation: 'e25302a6-72ed-41e9-a089-1789a1e7ff3c', - pdlCurrentCompany: '0ed142f6-f4b9-4517-a4ad-4c09da25ef21', pdlLinkedinUsername: 'b897abde-d051-459e-8761-1f8cdc2a93a5', pdlFacebookUrl: '733a1f4e-43d7-4450-aaee-03436c6f3df8', pdlProfiles: '63fd9ead-07a0-42b0-b230-17fb5c1b2666', @@ -36,16 +43,9 @@ export const PDL_FIELD_UNIVERSAL_IDENTIFIERS = { pdlJobSummary: 'cb5e077d-fd2a-4fb5-a927-8d789b3cae96', pdlInferredSalary: 'a89e9da9-9dd0-4097-b444-f9c1cedadf35', pdlIndustry: '19e667f0-cb94-4525-bbc0-2aa3a17c34d4', - pdlJobHistory: 'cac8449c-8072-4ec4-a87a-764012dc7e58', pdlCertifications: '02006f53-8c32-48a0-9738-73c38ad9412d', pdlSummary: '243d13f9-74e7-4638-9f9a-33fe76d584dd', pdlJobOnetCode: '48f70f1b-cad8-44e4-a27e-5dc26edfd391', - pdlJobCompanyName: '1426a17d-09c2-48aa-bd49-3adffeb2ec59', - pdlJobCompanyId: '3c8cd41b-1974-4da0-975c-1b3c18c436d9', - pdlJobCompanyWebsite: '7e44f20d-2443-4e33-999d-9c3b575112e1', - pdlJobCompanyLinkedinUrl: '3035757a-c93a-4d1f-9fb2-e4b581ea6cfa', - pdlJobCompanyIndustry: '4c37d4f1-3bf9-4881-afce-bbbc1e113832', - pdlJobCompanySize: '1c7c4e5a-fafa-4c7b-b4a7-3bf0b0ca07c5', pdlLocationMetro: 'f5e7434d-c8f5-4eab-866b-59693c9e7111', pdlLikelihood: '50583b08-f422-4964-a9ff-a4e4d6f7a6f0', pdlEnrichmentStatus: 'f03774c1-2b8b-4c62-89af-6add78efc4df', @@ -66,10 +66,10 @@ export const PDL_FIELD_UNIVERSAL_IDENTIFIERS = { pdlFundingStages: 'ced2c9d6-93ea-40de-9636-c0e719b9dc0b', pdlRawPayload: 'cc5bb8e5-c60d-4791-b838-3526942d6548', pdlLastEnrichedAt: '4fd64027-0ba9-4d2c-85eb-26ff90b7c721', - pdlCurrentEmployees: 'e654855f-1f10-462b-9721-cdf953c9b1bb', pdlLinkedinId: 'dd57bf00-e5b5-414d-a811-5480dac3b537', pdlFacebookUrl: '4f7fe9ad-5c4f-451c-9dc0-d368e669879f', pdlAlternativeNames: 'c53914e4-58b3-4fc2-b04b-94305675e130', + pdlLegalName: '6eaf9385-75a5-4245-9fa0-1b6d9c5fe136', pdlAlternativeDomains: '5bc022b8-e2d4-44f1-92d4-18034685c512', pdlSummary: '9d9e23f1-e781-4765-8dc9-3d5b885e231a', pdlIndustry: 'fe645516-ca6a-4b3c-aaa0-1bac05bb9ebc', @@ -86,6 +86,17 @@ export const PDL_FIELD_UNIVERSAL_IDENTIFIERS = { }, } as const; +export const PDL_VIEW_UNIVERSAL_IDENTIFIERS = { + enrichedCompanies: '25e3e91c-67cb-4a2c-b04d-3e93402d85ed', + enrichedPeople: '2866db8e-086a-4b2e-8b89-a583c4181936', +} as const; + +export const PDL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIERS = { + folder: '0a8fe0ea-7a97-41d7-8741-511d69ebe71b', + enrichedCompanies: 'b31eced0-0b68-444f-b823-594694b758f0', + enrichedPeople: 'ddf5b795-283a-4d5d-885c-c47f0bd11fd0', +} as const; + export const PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS = { personEnrichmentStatus: { matched: '59f2df76-5798-42e2-b647-5091b08a43f1', @@ -1090,155 +1101,6 @@ export const PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS = { wireless: '3702d3db-f99b-44a3-b7ff-4bea043050db', writingAndEditing: 'e25ffec5-db05-4488-85a6-4578c9ce1c9c', }, - personJobCompanyIndustry: { - accounting: 'c7646bb3-309c-4f7e-bd92-1145cd8fc53e', - airlinesAviation: 'ce4b50c2-81ce-473f-832c-d03dad5eb15c', - alternativeDisputeResolution: 'f3adb3b8-6d55-44f9-83d2-8c314f9e1b7f', - alternativeMedicine: 'e50695b4-e961-424c-ba35-f1552afb2972', - animation: '3025feb7-36ef-4c7c-94f3-0e6d8c198b12', - apparelFashion: '25f1bcec-3c54-48e7-8f27-5d1c905fc8b9', - architecturePlanning: 'ab4dee7a-3b6c-4494-b9c1-bd6cece38372', - artsAndCrafts: 'c61baa2c-3fba-444b-ab7d-a057b15a86d0', - automotive: 'dbc0cb0b-4d91-4ed9-be79-51bbf07e0ad2', - aviationAerospace: '73bf83ab-7ae0-438e-8c16-aff98d7a1bf5', - banking: 'b8358792-323c-4d65-af43-a550fba41896', - biotechnology: 'caa1993b-b5bc-4a38-bb35-461e9ac64cd9', - broadcastMedia: 'ebce7aa1-6d24-48f4-afa9-1ce469496447', - buildingMaterials: 'd0b16ab4-690a-4676-b88d-69b1879e5d6e', - businessSuppliesAndEquipment: '98c3aa81-d777-4b09-b94d-bf3a64214917', - capitalMarkets: 'af0ce50f-8d50-489e-962a-cf360bdb4b69', - chemicals: '11a423de-7ddd-466e-8914-bd8cd54e8a14', - civicSocialOrganization: 'e81d39f0-5411-49ab-90ab-d6fc1341d7f5', - civilEngineering: '30d1ce66-d5f9-4839-af59-0beca8b77bc4', - commercialRealEstate: '907f9a17-4090-4ff7-ad92-1fb1f2e14d7d', - computerNetworkSecurity: '6bd9dfae-fa74-48f1-83c9-59005daaa95a', - computerGames: '6587be1f-4d50-4155-846f-ea26da225f40', - computerHardware: 'fff38d7e-a166-4c6a-96b7-f28209b73136', - computerNetworking: 'f172e9f0-4217-4f59-ba31-0d6eaac5939f', - computerSoftware: '40092912-7856-4a92-b39f-8f17c7d477a7', - construction: '847712c3-37e8-4030-9735-5a04fccef373', - consumerElectronics: 'b76caec2-ce78-4aec-a1a5-d8a1b7a5b337', - consumerGoods: '3e59570f-4791-4545-aea8-1992cdc7af08', - consumerServices: '53ddcc27-a1d1-426e-9950-172ad4ac53f5', - cosmetics: '27740b0f-76a5-477d-a7e3-6335e5d1886f', - dairy: 'fdccb7e6-033f-460a-a237-e1b3ff8eeeb3', - defenseSpace: 'd87f7ba9-4f64-4b1c-a3cb-e4b26ffe9b28', - design: 'c58ea607-1436-42c6-b49b-01bede5d65be', - eLearning: '4dd2bbef-04f5-41a1-852c-a9066e9b56f4', - educationManagement: '5f99d759-6789-4e32-967b-53de2d247e65', - electricalElectronicManufacturing: '8ba94c6e-3d42-45d1-8a37-abfe0c916b09', - entertainment: '2534f3b2-2efd-4399-9189-ae0e80dea0b8', - environmentalServices: '4899a93c-912c-4969-a86d-481a9976cf7b', - eventsServices: '7a3af453-5241-4dd4-9962-4355a1569d26', - executiveOffice: 'e5563e78-6ebf-4ab4-a094-83f67b110c2a', - facilitiesServices: '4794543b-ade7-4975-a723-85d0c3c25e1f', - farming: '8ebd9eab-4fa9-45e4-8763-2386aa704729', - financialServices: '352126e6-b731-4531-bb55-45ed262b8790', - fineArt: 'e797e22f-f467-4e02-9678-6895183b12af', - fishery: 'f20950cd-5338-48b2-a368-19e3c5f54090', - foodBeverages: '0d25b31f-9715-4ef9-bb08-9332cd111a49', - foodProduction: '3509b838-e3e4-4b85-a2f7-c9350197a14e', - fundRaising: 'fd0d42aa-0b4e-46ef-b319-389bd77968d0', - furniture: '3e4a3b44-edeb-4266-aad7-64524e3db97b', - gamblingCasinos: '5e7bd38d-f0e1-4b8a-9085-d62bbbe5210d', - glassCeramicsConcrete: 'c0e67757-aaf3-40b3-833b-80c76f37c51c', - governmentAdministration: '930f7ae7-063a-429a-8130-6e4aed9b8f41', - governmentRelations: '26fe8ba1-c71a-4ad3-8026-ec3c147effe1', - graphicDesign: '124ba3b2-2557-4e63-9090-62ec0963f8d3', - healthWellnessAndFitness: 'a40465be-5a7a-44e3-a823-f5b9419c5be0', - higherEducation: 'cf1b074d-8cf5-484f-828e-2d2f9d037ad2', - hospitalHealthCare: '059bd3c7-88e6-468b-9842-81d33556ac17', - hospitality: '4df1f2ea-82a3-4ff4-890b-1cbe1f3203e7', - humanResources: '123531cc-7f88-4c0b-bd98-74c7fbe4ccbb', - importAndExport: '2053fa78-d93a-461d-8e84-695099a5e0b7', - individualFamilyServices: '48ecdf2b-faab-4979-904a-0d082bfcc83e', - industrialAutomation: 'be305ba6-fab3-4952-9fa0-2b4dc682b145', - informationServices: '40ae4e28-2c3a-416d-9f66-214808ec0cbf', - informationTechnologyAndServices: '58fc2d7c-a2e0-4284-b263-ff34f5a37d1b', - insurance: 'f3c59c70-6ad4-446e-97e1-27c5ea251c82', - internationalAffairs: '88b3db39-344c-4df7-a21b-9841d6d5e3c9', - internationalTradeAndDevelopment: 'f0b3eca1-3e03-402c-8398-ffcbd8148880', - internet: '26c060e8-e8bc-4ac6-a25a-e547cafd0050', - investmentBanking: '5ee3fbdc-2965-46f8-9b40-e748ce9cbe17', - investmentManagement: '76ffc3b2-6228-44fb-9cb9-9cf8cfff35ed', - judiciary: '373db33d-eae8-4946-a6b7-377a78bfc210', - lawEnforcement: 'cdaff85b-db82-4ef2-9493-51ab9523076b', - lawPractice: 'b26a48d2-1fec-444a-a39c-64b476588899', - legalServices: 'a7d8ff6a-a754-4a05-9830-d9b20e34160c', - legislativeOffice: '3f80fd4c-d564-468e-9808-9b43cb3167d0', - leisureTravelTourism: '3e7560d7-77af-4181-9f7e-38bb8d17a8b4', - libraries: '988b13ae-7bb1-472b-a991-bf040d079b11', - logisticsAndSupplyChain: 'c6cb1c2c-53d0-46f0-8004-0d9962d6106d', - luxuryGoodsJewelry: '15ac9e52-adb5-48e3-893d-87789511a966', - machinery: '509fbd7a-0a44-4f94-aa5c-685cce3faba4', - managementConsulting: '3a3d9a9b-f72c-4cde-bb76-ef9458bdc516', - maritime: '028d0304-43ba-48f6-aac9-33971bd5aac2', - marketResearch: '32f05431-ca91-456f-a1cf-8e45f64be5d2', - marketingAndAdvertising: '6e0c1593-b08a-4fe2-a985-186706777c13', - mechanicalOrIndustrialEngineering: '72a8c609-7ed2-454d-ae28-5ed7c10932ab', - mediaProduction: 'd82defb2-294b-4552-af09-0d0b3e3f87e4', - medicalDevices: 'efe813c7-5d54-4a8a-b97e-4290f670d1da', - medicalPractice: '58bad1fd-667f-40ca-ba18-aac0396a02b4', - mentalHealthCare: 'f03985f0-5cc7-4850-b97c-a7ea425cbee0', - military: '4a410e16-54ef-4312-bd3f-36c2ee36943e', - miningMetals: 'cd00c312-130f-49c8-9942-00ea24553ba5', - motionPicturesAndFilm: '5d935c3c-c8a3-4e51-a593-da3dbbab5783', - museumsAndInstitutions: 'd4c2a66f-88ae-4e2a-b3fd-855951d0376c', - music: '74cebb0e-dfa6-4fe4-9e10-2055d7265078', - nanotechnology: '1f02fa05-1464-4e32-9090-230ad2d0557b', - newspapers: 'b62a3423-075c-4bf9-a78c-528754df2841', - nonProfitOrganizationManagement: '92ae1c10-12c5-423d-bb73-6f7730c5d282', - oilEnergy: '3bea7331-2355-4a37-8604-3e83a363ef1b', - onlineMedia: '2f893d39-9975-44cf-a607-072be812d9a4', - outsourcingOffshoring: '29f227ec-2a83-418a-8bde-7627dd25add3', - packageFreightDelivery: '652bfc40-a5be-4191-a9ed-10db3f5ba059', - packagingAndContainers: '223e7263-2e3a-4a39-bef0-5a5c95759d7d', - paperForestProducts: '083402a8-f06d-4b0f-9654-2d4e0c51d4ae', - performingArts: 'd74ee411-6486-47ba-b0e2-56e606249a91', - pharmaceuticals: '99a8dae3-60a3-4cd5-80e6-a83651a28146', - philanthropy: '1744e009-9330-42e4-98ad-23da6d1cc898', - photography: 'a93a38e3-847e-4280-bb9a-cf7269115e13', - plastics: '8b06307f-df3b-45e2-adf6-f01b6c9a44a2', - politicalOrganization: '1a40650c-ed34-4d3e-97b9-70bfd48b1606', - primarySecondaryEducation: 'aeaa7034-3847-4e2b-83c6-5ad3c2c7370b', - printing: '29c4dd98-5f87-436f-8a9a-846daf36e648', - professionalTrainingCoaching: 'ddc493cb-208f-4c8f-b122-23cbe45c0ffe', - programDevelopment: 'e83e628a-9f57-4727-925f-37114ce125c1', - publicPolicy: '58c6e64d-e5a9-495d-b3ed-87987f976c4f', - publicRelationsAndCommunications: '19bc1020-8038-4be4-a4b5-be85e54c7535', - publicSafety: '69f06a55-17d5-4c4c-94ba-44625ec8e1f5', - publishing: '95e47f5e-d02b-4707-8510-c1310158260b', - railroadManufacture: '1eff24a9-db0f-463a-a9c6-2bdebd760ec6', - ranching: 'd3f2e4ad-d933-4cc0-ae2d-65f925bee53d', - realEstate: '9b81a975-c52e-46d2-b853-334a484f24e5', - recreationalFacilitiesAndServices: 'a2b4ba9b-65b7-4f06-8f99-78b4b03a5a8b', - religiousInstitutions: '9dc283bc-dfb2-4a7f-bce7-433ce0e4c22d', - renewablesEnvironment: 'dffe3cc0-13d3-48f4-8a72-f8f5509d97e5', - research: 'f3a47e4f-30c4-4b02-8cb0-8d493518ce57', - restaurants: 'ec0efd93-dffe-465d-9dfe-d23e91c3aa71', - retail: '143fdd79-0167-4701-b910-0640b69e6909', - securityAndInvestigations: '4072f339-ee30-465a-af76-0f58d8469e25', - semiconductors: 'c1103955-5e09-4f78-bfc2-9366c18bf22e', - shipbuilding: '2d6481c3-d46b-4e67-9328-dd2e22eacc02', - sportingGoods: 'ed197d67-bf55-4e50-a1ca-264c93a62961', - sports: '5183468a-5b8d-4442-9ea3-9a45de9a7630', - staffingAndRecruiting: '5e610095-5d70-4816-a56d-b9fc0705e0b0', - supermarkets: '4531f90a-d45a-4f84-8066-da2987fc46e6', - telecommunications: '94c7b837-0f22-4fca-a7a2-8951cf869c81', - textiles: 'a09422a2-e181-4faa-b904-3816b52bb7ee', - thinkTanks: 'c434dbe6-fa9a-4af1-8b40-9165b6a34530', - tobacco: '16ebae0f-59fc-4b8d-932f-840d54ae8b5c', - translationAndLocalization: 'b94949a2-bf18-4cd3-943a-c381540d088e', - transportationTruckingRailroad: '19882453-428c-4179-8614-a5e43dd40e5a', - utilities: '911dfaf3-d735-4fdb-92c0-d038dc993144', - ventureCapitalPrivateEquity: '478e405c-b780-45a9-9979-0910a9d10a40', - veterinary: '2ed603fb-0491-4386-82ca-d5e43bbcdd51', - warehousing: 'e0758679-11f6-45cf-af21-da21e297edbc', - wholesale: '14d5d2c2-cd70-4436-9e42-2cb913f942e8', - wineAndSpirits: 'd470d512-1736-4fe2-8c96-af30d8f03e60', - wireless: 'dcf165e4-c123-436f-bd52-b6ddafcb3d74', - writingAndEditing: 'a78d7014-682e-4455-a3d1-9f0c4bb21b01', - }, companyIndustry: { accounting: '67406afe-dd7e-4a39-a86b-394e1f3b7428', airlinesAviation: '6e5ea79c-fc34-4fd1-aaf0-67d94585ad84', @@ -1516,16 +1378,6 @@ export const PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS = { from150000To250000: '063ce36e-51ff-4dc2-86eb-3a4b2f823297', over250000: 'fc3658bf-dec7-443b-84f3-0770f3aae62f', }, - personJobCompanySize: { - oneToTen: '9a958c46-b3e3-4e36-ba2f-08b5e0dd1ee7', - elevenToFifty: 'b383e032-cf52-4082-ba2e-67b02228e283', - fiftyOneToTwoHundred: '32fb12cb-49aa-4fbf-b683-365cea2f8d30', - twoHundredOneToFiveHundred: '333ec4cb-e922-4b80-8830-13bf89d600a0', - fiveHundredOneToOneThousand: '8d1612c8-5991-4d98-8901-f86effd8bd79', - oneThousandOneToFiveThousand: '77383346-4ec3-4c45-9473-4687ee8d58af', - fiveThousandOneToTenThousand: '9d654179-01b3-4694-ac24-f03f14023d4a', - tenThousandOnePlus: '2ff37073-c0ca-4893-ac0c-d0f52cce1ee2', - }, companyLocationContinent: { africa: '0efd52cc-6ee3-4890-81ab-0a12a6c56d81', antarctica: '68bdf16c-bf40-445f-86b6-093c51ed6416', @@ -1595,7 +1447,7 @@ export const PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS = { fiveThousandOneToTenThousand: '9e730fed-d391-40f1-aafb-c85cf2152c82', tenThousandOnePlus: '69460054-e739-430f-aad3-ab3b694a59ea', }, - fundingStage: { + companyLatestFundingStage: { angel: '88f53a5b-4f12-468c-9f31-c5204487be33', convertibleNote: '0299706c-bd3b-44b9-bd1b-cab4b32528d8', corporateRound: 'f3a24a44-80ab-4ed5-bdd4-6ce01b80abba', @@ -1626,4 +1478,35 @@ export const PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS = { seriesUnknown: 'dc64f5a8-c3d2-47f2-b8a9-5b7476d5dba5', undisclosed: '504b07bb-4ed8-4116-8d7d-9bc6068b2ad9', }, + companyFundingStages: { + angel: '8a9a60d8-e447-4958-a992-12b3a010a841', + convertibleNote: '19cc27ea-d0ae-4799-afc2-c365930a60f3', + corporateRound: 'd171c11e-6a43-47ee-861c-2df2abc5dd99', + debtFinancing: '11b48874-9006-4a58-a65b-90ba3e3ebc0a', + equityCrowdfunding: '59a2f7ec-0114-4c9f-a8ac-ac0e5878f598', + fundingRound: 'b394e6e9-e780-4cbf-84a0-2b1ab6f5896e', + grant: 'de9d2fb3-764c-4a06-8ec0-e3112f340039', + initialCoinOffering: 'ccc6e792-4fa4-406d-af09-c25ce10159bb', + nonEquityAssistance: '6cbc164f-d987-4464-8412-4ee14ce34711', + postIpoDebt: 'c140ed63-e83a-4bbc-b61d-b68e174522cd', + postIpoEquity: 'f918198a-3be3-4f3d-a604-9b9a1036c456', + postIpoSecondary: 'df18a9ea-b8e4-402e-8cd1-7590c4e68188', + preSeed: '199712b6-641c-441a-9b7d-50c9e1715911', + privateEquity: 'fb95dd72-235a-45b8-972f-41f5b6ed51a8', + productCrowdfunding: 'e346506b-69e2-45b5-a001-f43cb9761f80', + secondaryMarket: '5f7a3a7e-a4fc-4154-93d7-3bd0eaf8ec9a', + seed: '7fc9e87a-9368-40aa-ac28-c791161e77c8', + seriesA: 'ea4e4977-efbf-4ae3-90c4-297b28278220', + seriesB: '722ae721-cb80-441d-a75d-78b94c8a8f9f', + seriesC: '6775116c-9764-44e2-b475-48ae00c4b4fd', + seriesD: '5c773ff1-02b9-4f73-9687-fc24421b0928', + seriesE: 'a6282849-b214-4b57-8d23-77d8465b597e', + seriesF: 'f16068b0-4a64-460a-a650-5e7bfd692af2', + seriesG: '384edc24-eef7-40cb-8cc9-4961f4940d73', + seriesH: '4dbec72f-ad9a-42db-bf85-55bf516a301e', + seriesI: '8c96ba47-eaf1-4490-be9f-64c729fff359', + seriesJ: 'bf56435c-36eb-484f-a08d-8a1c894d962b', + seriesUnknown: 'e09c30f6-1c80-4696-a929-526126ab1e94', + undisclosed: '5f40471e-6a75-4a0a-8e4b-039c5509314e', + }, } as const; diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-affiliated-profiles.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-affiliated-profiles.field.ts index fc8ff5aaf2e31..759b3aff1b99b 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-affiliated-profiles.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-affiliated-profiles.field.ts @@ -13,7 +13,8 @@ export default defineField({ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, type: FieldType.ARRAY, name: 'pdlAffiliatedProfiles', - label: 'Affiliated Profiles', - description: 'Affiliated company profiles returned by People Data Labs.', + label: 'Profiles', + description: 'Web and social profile URLs returned by People Data Labs.', + icon: 'IconWorldWww', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-alternative-domains.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-alternative-domains.field.ts index 08fc671beff78..0086512925af8 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-alternative-domains.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-alternative-domains.field.ts @@ -15,5 +15,6 @@ export default defineField({ name: 'pdlAlternativeDomains', label: 'Alternative Domains', description: 'Alternative domains returned by People Data Labs.', + icon: 'IconWorldWww', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-alternative-names.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-alternative-names.field.ts index 7b257411f425b..3ff763fb7ecdf 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-alternative-names.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-alternative-names.field.ts @@ -15,5 +15,6 @@ export default defineField({ name: 'pdlAlternativeNames', label: 'Alternative Names', description: 'Alternative names returned by People Data Labs.', + icon: 'IconSignature', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-company-type.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-company-type.field.ts index 01a57e9840a16..69e38731f7989 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-company-type.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-company-type.field.ts @@ -4,10 +4,12 @@ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; +import { COMPANY_TYPE_OPTIONS } from 'src/constants/company-type-options'; import { PDL_FIELD_UNIVERSAL_IDENTIFIERS, PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, } from 'src/constants/universal-identifiers'; +import { buildSelectOptions } from 'src/utils/build-select-options'; export default defineField({ universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlCompanyType, @@ -17,49 +19,10 @@ export default defineField({ name: 'pdlCompanyType', label: 'Company Type', description: 'People Data Labs canonical company type.', + icon: 'IconBuildingCommunity', isNullable: true, - options: [ - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.public, - value: 'PUBLIC', - label: 'Public', - color: 'blue', - position: 0, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.private, - value: 'PRIVATE', - label: 'Private', - color: 'red', - position: 1, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.publicSubsidiary, - value: 'PUBLIC_SUBSIDIARY', - label: 'Public Subsidiary', - color: 'green', - position: 2, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.educational, - value: 'EDUCATIONAL', - label: 'Educational', - color: 'orange', - position: 3, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.government, - value: 'GOVERNMENT', - label: 'Government', - color: 'purple', - position: 4, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.nonprofit, - value: 'NONPROFIT', - label: 'Nonprofit', - color: 'yellow', - position: 5, - }, - ], + options: buildSelectOptions({ + meta: COMPANY_TYPE_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-current-employees.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-current-employees.field.ts deleted file mode 100644 index 52c92ee97e6b6..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-current-employees.field.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - defineField, - FieldType, - RelationType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineField({ - universalIdentifier: - PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlCurrentEmployees, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, - type: FieldType.RELATION, - name: 'pdlCurrentEmployees', - label: 'PDL Current Employees', - description: - 'People whose current employer is this company, per People Data Labs.', - relationTargetObjectMetadataUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - relationTargetFieldMetadataUniversalIdentifier: - PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlCurrentCompany, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, - icon: 'IconUsers', -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-employee-count-by-country.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-employee-count-by-country.field.ts index c45142b6c49f4..74262a8ff48f8 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-employee-count-by-country.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-employee-count-by-country.field.ts @@ -16,5 +16,6 @@ export default defineField({ label: 'Employee Count by Country', description: 'Employee count broken down by country returned by People Data Labs.', + icon: 'IconMapPins', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-employee-count.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-employee-count.field.ts index 93d74bba660cb..357e71f8392c6 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-employee-count.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-employee-count.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlEmployeeCount', label: 'Employees', description: 'Exact employee count returned by People Data Labs.', + icon: 'IconUsersGroup', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-enrichment-status.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-enrichment-status.field.ts index 6ec7d372a82a0..d0abeb9061943 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-enrichment-status.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-enrichment-status.field.ts @@ -20,9 +20,10 @@ export default defineField({ name: 'pdlEnrichmentStatus', label: 'Enrichment Status', description: 'Outcome of the latest People Data Labs enrichment attempt.', + icon: 'IconProgressCheck', isNullable: true, - options: buildSelectOptions( - ENRICHMENT_STATUS_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyEnrichmentStatus, - ), + options: buildSelectOptions({ + meta: ENRICHMENT_STATUS_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyEnrichmentStatus, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-facebook-url.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-facebook-url.field.ts index 3e665e242393c..74dc15b341ca4 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-facebook-url.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-facebook-url.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlFacebookUrl', label: 'Facebook', description: 'Facebook page returned by People Data Labs.', + icon: 'IconBrandFacebook', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-founded-year.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-founded-year.field.ts index 232b845c787bc..431193c8df29f 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-founded-year.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-founded-year.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlFoundedYear', label: 'Founded Year', description: 'Founding year returned by People Data Labs.', + icon: 'IconFlag', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-funding-stages.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-funding-stages.field.ts index cbbddc33db781..c90fa014126e4 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-funding-stages.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-funding-stages.field.ts @@ -19,9 +19,10 @@ export default defineField({ name: 'pdlFundingStages', label: 'Funding Stages', description: 'People Data Labs canonical funding stages.', + icon: 'IconChartArrowsVertical', isNullable: true, - options: buildSelectOptions( - FUNDING_STAGE_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.fundingStage, - ), + options: buildSelectOptions({ + meta: FUNDING_STAGE_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyFundingStages, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-headline.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-headline.field.ts index 0acfe0292c070..a660db514a1c2 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-headline.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-headline.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlHeadline', label: 'Headline', description: 'People Data Labs company headline or summary.', + icon: 'IconQuote', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-id.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-id.field.ts index 5e23cc3b43f5e..0724d3457f9e9 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-id.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-id.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlId', label: 'PDL ID', description: 'People Data Labs company identifier.', + icon: 'IconId', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-industry-detail.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-industry-detail.field.ts index dfbda512c3bf7..e062fe4136b8e 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-industry-detail.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-industry-detail.field.ts @@ -15,5 +15,6 @@ export default defineField({ name: 'pdlIndustryDetail', label: 'Industry (detailed)', description: 'Detailed People Data Labs industry v2 value.', + icon: 'IconBuildingFactory', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-industry.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-industry.field.ts index 9fba3d54f8f4a..deec3af2bcf03 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-industry.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-industry.field.ts @@ -19,9 +19,10 @@ export default defineField({ name: 'pdlIndustry', label: 'Industry', description: 'People Data Labs canonical industry.', + icon: 'IconBuildingFactory2', isNullable: true, - options: buildSelectOptions( - INDUSTRY_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyIndustry, - ), + options: buildSelectOptions({ + meta: INDUSTRY_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyIndustry, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-last-enriched-at.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-last-enriched-at.field.ts index c25fcb6be3d93..3d031a4b8db5d 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-last-enriched-at.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-last-enriched-at.field.ts @@ -15,5 +15,6 @@ export default defineField({ name: 'pdlLastEnrichedAt', label: 'Last Enriched At', description: 'Timestamp of the latest People Data Labs enrichment.', + icon: 'IconClock', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-last-funding-date.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-last-funding-date.field.ts index 169052206177d..182f43fa866a0 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-last-funding-date.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-last-funding-date.field.ts @@ -15,5 +15,6 @@ export default defineField({ name: 'pdlLastFundingDate', label: 'Last Funding Date', description: 'Date of the latest funding round returned by People Data Labs.', + icon: 'IconCalendar', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-latest-funding-stage.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-latest-funding-stage.field.ts index 71e89795f6684..81d1619f242f6 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-latest-funding-stage.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-latest-funding-stage.field.ts @@ -20,9 +20,10 @@ export default defineField({ name: 'pdlLatestFundingStage', label: 'Latest Funding Stage', description: 'People Data Labs canonical latest funding stage.', + icon: 'IconTrendingUp', isNullable: true, - options: buildSelectOptions( - FUNDING_STAGE_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.fundingStage, - ), + options: buildSelectOptions({ + meta: FUNDING_STAGE_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLatestFundingStage, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-name.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-legal-name.field.ts similarity index 50% rename from packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-name.field.ts rename to packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-legal-name.field.ts index 3e9d5761a7c4e..4082b2d16ff47 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-name.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-legal-name.field.ts @@ -7,12 +7,13 @@ import { import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; export default defineField({ - universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanyName, + universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLegalName, objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, type: FieldType.TEXT, - name: 'pdlJobCompanyName', - label: 'Current Company', - description: 'Current company name returned by People Data Labs.', + name: 'pdlLegalName', + label: 'Legal Name', + description: 'Registered legal entity name returned by People Data Labs.', + icon: 'IconBuildingBank', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-linkedin-id.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-linkedin-id.field.ts index 7c3be81c22a7d..39a134f0dab8d 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-linkedin-id.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-linkedin-id.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlLinkedinId', label: 'LinkedIn ID', description: 'LinkedIn identifier returned by People Data Labs.', + icon: 'IconBrandLinkedin', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-location-continent.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-location-continent.field.ts index 4cdb806e2d9a4..296e6975262d6 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-location-continent.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-location-continent.field.ts @@ -4,10 +4,12 @@ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; +import { LOCATION_CONTINENT_OPTIONS } from 'src/constants/location-continent-options'; import { PDL_FIELD_UNIVERSAL_IDENTIFIERS, PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, } from 'src/constants/universal-identifiers'; +import { buildSelectOptions } from 'src/utils/build-select-options'; export default defineField({ universalIdentifier: @@ -18,62 +20,10 @@ export default defineField({ name: 'pdlLocationContinent', label: 'Continent', description: 'People Data Labs canonical continent.', + icon: 'IconGlobe', isNullable: true, - options: [ - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent - .africa, - value: 'AFRICA', - label: 'Africa', - color: 'blue', - position: 0, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent - .antarctica, - value: 'ANTARCTICA', - label: 'Antarctica', - color: 'red', - position: 1, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent.asia, - value: 'ASIA', - label: 'Asia', - color: 'green', - position: 2, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent - .europe, - value: 'EUROPE', - label: 'Europe', - color: 'orange', - position: 3, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent - .northAmerica, - value: 'NORTH_AMERICA', - label: 'North America', - color: 'purple', - position: 4, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent - .oceania, - value: 'OCEANIA', - label: 'Oceania', - color: 'yellow', - position: 5, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent - .southAmerica, - value: 'SOUTH_AMERICA', - label: 'South America', - color: 'pink', - position: 6, - }, - ], + options: buildSelectOptions({ + meta: LOCATION_CONTINENT_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-location-metro.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-location-metro.field.ts index 9406279d402f7..c299c1dea856d 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-location-metro.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-location-metro.field.ts @@ -19,9 +19,10 @@ export default defineField({ name: 'pdlLocationMetro', label: 'Metro Area', description: 'People Data Labs canonical metro area.', + icon: 'IconMap2', isNullable: true, - options: buildSelectOptions( - METRO_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationMetro, - ), + options: buildSelectOptions({ + meta: METRO_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationMetro, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-mic-exchange.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-mic-exchange.field.ts index d0951fc94e10a..561f3e9c064c2 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-mic-exchange.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-mic-exchange.field.ts @@ -4,10 +4,12 @@ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; +import { MIC_EXCHANGE_OPTIONS } from 'src/constants/mic-exchange-options'; import { PDL_FIELD_UNIVERSAL_IDENTIFIERS, PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, } from 'src/constants/universal-identifiers'; +import { buildSelectOptions } from 'src/utils/build-select-options'; export default defineField({ universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlMicExchange, @@ -17,497 +19,10 @@ export default defineField({ name: 'pdlMicExchange', label: 'Stock Exchange (MIC)', description: 'People Data Labs canonical market identifier code.', + icon: 'IconBuildingBank', isNullable: true, - options: [ - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.aqse, - value: 'AQSE', - label: 'AQSE', - color: 'blue', - position: 0, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.arcx, - value: 'ARCX', - label: 'ARCX', - color: 'red', - position: 1, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.asex, - value: 'ASEX', - label: 'ASEX', - color: 'green', - position: 2, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.bcxe, - value: 'BCXE', - label: 'BCXE', - color: 'orange', - position: 3, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.bvmf, - value: 'BVMF', - label: 'BVMF', - color: 'purple', - position: 4, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.dsmd, - value: 'DSMD', - label: 'DSMD', - color: 'yellow', - position: 5, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.misx, - value: 'MISX', - label: 'MISX', - color: 'pink', - position: 6, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.neoe, - value: 'NEOE', - label: 'NEOE', - color: 'cyan', - position: 7, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xams, - value: 'XAMS', - label: 'XAMS', - color: 'brown', - position: 8, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xase, - value: 'XASE', - label: 'XASE', - color: 'lime', - position: 9, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xasx, - value: 'XASX', - label: 'XASX', - color: 'violet', - position: 10, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xber, - value: 'XBER', - label: 'XBER', - color: 'gold', - position: 11, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbkk, - value: 'XBKK', - label: 'XBKK', - color: 'turquoise', - position: 12, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbog, - value: 'XBOG', - label: 'XBOG', - color: 'crimson', - position: 13, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbom, - value: 'XBOM', - label: 'XBOM', - color: 'sky', - position: 14, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbru, - value: 'XBRU', - label: 'XBRU', - color: 'amber', - position: 15, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbsp, - value: 'XBSP', - label: 'XBSP', - color: 'plum', - position: 16, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbud, - value: 'XBUD', - label: 'XBUD', - color: 'grass', - position: 17, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbue, - value: 'XBUE', - label: 'XBUE', - color: 'tomato', - position: 18, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcai, - value: 'XCAI', - label: 'XCAI', - color: 'iris', - position: 19, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcbo, - value: 'XCBO', - label: 'XCBO', - color: 'mint', - position: 20, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcnq, - value: 'XCNQ', - label: 'XCNQ', - color: 'ruby', - position: 21, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcse, - value: 'XCSE', - label: 'XCSE', - color: 'bronze', - position: 22, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xdfm, - value: 'XDFM', - label: 'XDFM', - color: 'jade', - position: 23, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xdub, - value: 'XDUB', - label: 'XDUB', - color: 'gray', - position: 24, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xdus, - value: 'XDUS', - label: 'XDUS', - color: 'blue', - position: 25, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xeqy, - value: 'XEQY', - label: 'XEQY', - color: 'red', - position: 26, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xfra, - value: 'XFRA', - label: 'XFRA', - color: 'green', - position: 27, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xham, - value: 'XHAM', - label: 'XHAM', - color: 'orange', - position: 28, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xhel, - value: 'XHEL', - label: 'XHEL', - color: 'purple', - position: 29, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xhkg, - value: 'XHKG', - label: 'XHKG', - color: 'yellow', - position: 30, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xice, - value: 'XICE', - label: 'XICE', - color: 'pink', - position: 31, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xidx, - value: 'XIDX', - label: 'XIDX', - color: 'cyan', - position: 32, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xjpx, - value: 'XJPX', - label: 'XJPX', - color: 'brown', - position: 33, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xjse, - value: 'XJSE', - label: 'XJSE', - color: 'lime', - position: 34, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkls, - value: 'XKLS', - label: 'XKLS', - color: 'violet', - position: 35, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkos, - value: 'XKOS', - label: 'XKOS', - color: 'gold', - position: 36, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkrx, - value: 'XKRX', - label: 'XKRX', - color: 'turquoise', - position: 37, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkuw, - value: 'XKUW', - label: 'XKUW', - color: 'crimson', - position: 38, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xlis, - value: 'XLIS', - label: 'XLIS', - color: 'sky', - position: 39, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xlon, - value: 'XLON', - label: 'XLON', - color: 'amber', - position: 40, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmad, - value: 'XMAD', - label: 'XMAD', - color: 'plum', - position: 41, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmex, - value: 'XMEX', - label: 'XMEX', - color: 'grass', - position: 42, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmil, - value: 'XMIL', - label: 'XMIL', - color: 'tomato', - position: 43, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmun, - value: 'XMUN', - label: 'XMUN', - color: 'iris', - position: 44, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnas, - value: 'XNAS', - label: 'XNAS', - color: 'mint', - position: 45, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnse, - value: 'XNSE', - label: 'XNSE', - color: 'ruby', - position: 46, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnys, - value: 'XNYS', - label: 'XNYS', - color: 'bronze', - position: 47, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnze, - value: 'XNZE', - label: 'XNZE', - color: 'jade', - position: 48, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xosl, - value: 'XOSL', - label: 'XOSL', - color: 'gray', - position: 49, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xotc, - value: 'XOTC', - label: 'XOTC', - color: 'blue', - position: 50, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xpar, - value: 'XPAR', - label: 'XPAR', - color: 'red', - position: 51, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xpra, - value: 'XPRA', - label: 'XPRA', - color: 'green', - position: 52, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xris, - value: 'XRIS', - label: 'XRIS', - color: 'orange', - position: 53, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xsau, - value: 'XSAU', - label: 'XSAU', - color: 'purple', - position: 54, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xses, - value: 'XSES', - label: 'XSES', - color: 'yellow', - position: 55, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xsgo, - value: 'XSGO', - label: 'XSGO', - color: 'pink', - position: 56, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xshe, - value: 'XSHE', - label: 'XSHE', - color: 'cyan', - position: 57, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xshg, - value: 'XSHG', - label: 'XSHG', - color: 'brown', - position: 58, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xstc, - value: 'XSTC', - label: 'XSTC', - color: 'lime', - position: 59, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xsto, - value: 'XSTO', - label: 'XSTO', - color: 'violet', - position: 60, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xstu, - value: 'XSTU', - label: 'XSTU', - color: 'gold', - position: 61, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xswx, - value: 'XSWX', - label: 'XSWX', - color: 'turquoise', - position: 62, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtae, - value: 'XTAE', - label: 'XTAE', - color: 'crimson', - position: 63, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtai, - value: 'XTAI', - label: 'XTAI', - color: 'sky', - position: 64, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtal, - value: 'XTAL', - label: 'XTAL', - color: 'amber', - position: 65, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtse, - value: 'XTSE', - label: 'XTSE', - color: 'plum', - position: 66, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtsx, - value: 'XTSX', - label: 'XTSX', - color: 'grass', - position: 67, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xwar, - value: 'XWAR', - label: 'XWAR', - color: 'tomato', - position: 68, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xwbo, - value: 'XWBO', - label: 'XWBO', - color: 'iris', - position: 69, - }, - ], + options: buildSelectOptions({ + meta: MIC_EXCHANGE_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-naics.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-naics.field.ts index 350335888488b..90fea207335da 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-naics.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-naics.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlNaics', label: 'NAICS Codes', description: 'NAICS classification codes returned by People Data Labs.', + icon: 'IconNumbers', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-number-funding-rounds.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-number-funding-rounds.field.ts index 1a2c2d4135d5b..f3d1000d7e5bf 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-number-funding-rounds.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-number-funding-rounds.field.ts @@ -15,5 +15,6 @@ export default defineField({ name: 'pdlNumberFundingRounds', label: 'Number of Funding Rounds', description: 'Number of funding rounds returned by People Data Labs.', + icon: 'IconCoins', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-raw-payload.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-raw-payload.field.ts index 7718a058fcfa3..71444a7b5d57d 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-raw-payload.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-raw-payload.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlRawPayload', label: 'PDL Raw Payload', description: 'Full People Data Labs company enrichment response.', + icon: 'IconBraces', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-sic.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-sic.field.ts index 2273a1ecb0393..36e8e7dd98b56 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-sic.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-sic.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlSic', label: 'SIC Codes', description: 'SIC classification codes returned by People Data Labs.', + icon: 'IconHash', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-size-range.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-size-range.field.ts index 786c722289df9..bfce5bc39240d 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-size-range.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-size-range.field.ts @@ -19,9 +19,10 @@ export default defineField({ name: 'pdlSizeRange', label: 'Employee Range', description: 'People Data Labs canonical self-reported employee range.', + icon: 'IconChartBar', isNullable: true, - options: buildSelectOptions( - SIZE_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.sizeRange, - ), + options: buildSelectOptions({ + meta: SIZE_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.sizeRange, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-summary.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-summary.field.ts index cb2082ded481d..446b293edd97b 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-summary.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-summary.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlSummary', label: 'Summary', description: 'Company summary returned by People Data Labs.', + icon: 'IconFileText', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-tags.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-tags.field.ts index 33a7ce2cba085..b708d1f0bbe8d 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-tags.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-tags.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlTags', label: 'Tags', description: 'Tags returned by People Data Labs.', + icon: 'IconTags', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-ticker.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-ticker.field.ts index e68c26da2d409..8bd168e5a4f45 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-ticker.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-ticker.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlTicker', label: 'Ticker', description: 'Company ticker returned by People Data Labs.', + icon: 'IconChartCandle', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-total-funding.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-total-funding.field.ts index e6bc620b79c1d..71bf4a8b926d9 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-total-funding.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-total-funding.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlTotalFunding', label: 'Total Funding Raised', description: 'Total funding raised in USD returned by People Data Labs.', + icon: 'IconReportMoney', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-twitter-url.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-twitter-url.field.ts index 462a574b638aa..0b127c54628a5 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-twitter-url.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/company/pdl-twitter-url.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlTwitterUrl', label: 'X / Twitter', description: 'X or Twitter company profile returned by People Data Labs.', + icon: 'IconBrandX', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-birth-date.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-birth-date.field.ts index 0f53e78dcee6f..968d362f9fe7b 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-birth-date.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-birth-date.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlBirthDate', label: 'Birth Date', description: 'Birth date returned by People Data Labs.', + icon: 'IconCake', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-birth-year.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-birth-year.field.ts index b61c3da59e0a4..37937bfa1db36 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-birth-year.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-birth-year.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlBirthYear', label: 'Birth Year', description: 'Birth year returned by People Data Labs.', + icon: 'IconCalendar', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-certifications.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-certifications.field.ts index 029a52d43bfdd..3e8ef28b6fa3a 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-certifications.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-certifications.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlCertifications', label: 'Certifications', description: 'Certifications returned by People Data Labs.', + icon: 'IconCertificate', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-current-company.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-current-company.field.ts deleted file mode 100644 index 7d0d499e4d312..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-current-company.field.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - defineField, - FieldType, - RelationType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineField({ - universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlCurrentCompany, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - type: FieldType.RELATION, - name: 'pdlCurrentCompany', - label: 'PDL Current Company', - description: 'Current employer detected by People Data Labs.', - relationTargetObjectMetadataUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, - relationTargetFieldMetadataUniversalIdentifier: - PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlCurrentEmployees, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - }, - icon: 'IconBuilding', -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-education.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-education.field.ts index 5fd373ba25fc0..1a6104ad27222 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-education.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-education.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlEducation', label: 'Education History', description: 'Education history returned by People Data Labs.', + icon: 'IconSchool', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-enrichment-status.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-enrichment-status.field.ts index 995bfedc1b1b5..8545dc98e6121 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-enrichment-status.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-enrichment-status.field.ts @@ -20,9 +20,10 @@ export default defineField({ name: 'pdlEnrichmentStatus', label: 'Enrichment Status', description: 'Outcome of the latest People Data Labs enrichment attempt.', + icon: 'IconProgressCheck', isNullable: true, - options: buildSelectOptions( - ENRICHMENT_STATUS_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personEnrichmentStatus, - ), + options: buildSelectOptions({ + meta: ENRICHMENT_STATUS_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personEnrichmentStatus, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-experience.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-experience.field.ts index 118338805f2c5..91d5c66bb178b 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-experience.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-experience.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlExperience', label: 'Experience History', description: 'Experience history returned by People Data Labs.', + icon: 'IconTimeline', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-facebook-url.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-facebook-url.field.ts index e2ef6642341fc..50ad56a76302c 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-facebook-url.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-facebook-url.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlFacebookUrl', label: 'Facebook', description: 'Facebook profile returned by People Data Labs.', + icon: 'IconBrandFacebook', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-github-url.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-github-url.field.ts index dd55a53f19fbf..49c050b507004 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-github-url.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-github-url.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlGithubUrl', label: 'GitHub', description: 'GitHub profile returned by People Data Labs.', + icon: 'IconBrandGithub', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-headline.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-headline.field.ts index bc16f6701d5fc..701f0a11c24d3 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-headline.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-headline.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlHeadline', label: 'Headline', description: 'People Data Labs person headline or job summary.', + icon: 'IconQuote', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-id.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-id.field.ts index e80d878cec82c..9f61175ae391d 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-id.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-id.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlId', label: 'PDL ID', description: 'People Data Labs person identifier.', + icon: 'IconId', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-industry.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-industry.field.ts index 9a8a1c50c501a..28b3a70b5645a 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-industry.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-industry.field.ts @@ -19,9 +19,10 @@ export default defineField({ name: 'pdlIndustry', label: 'Industry', description: 'People Data Labs canonical industry.', + icon: 'IconBuildingFactory2', isNullable: true, - options: buildSelectOptions( - INDUSTRY_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personIndustry, - ), + options: buildSelectOptions({ + meta: INDUSTRY_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personIndustry, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-inferred-salary.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-inferred-salary.field.ts index 0c2d106e8f41c..5a969760e29d6 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-inferred-salary.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-inferred-salary.field.ts @@ -4,10 +4,12 @@ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; +import { INFERRED_SALARY_OPTIONS } from 'src/constants/inferred-salary-options'; import { PDL_FIELD_UNIVERSAL_IDENTIFIERS, PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, } from 'src/constants/universal-identifiers'; +import { buildSelectOptions } from 'src/utils/build-select-options'; export default defineField({ universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlInferredSalary, @@ -17,95 +19,10 @@ export default defineField({ name: 'pdlInferredSalary', label: 'Inferred Salary (range)', description: 'People Data Labs canonical inferred salary range.', + icon: 'IconCurrencyDollar', isNullable: true, - options: [ - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .under20000, - value: 'UNDER_20000', - label: '<20,000', - color: 'blue', - position: 0, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .from20000To25000, - value: 'FROM_20000_TO_25000', - label: '20,000-25,000', - color: 'red', - position: 1, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .from25000To35000, - value: 'FROM_25000_TO_35000', - label: '25,000-35,000', - color: 'green', - position: 2, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .from35000To45000, - value: 'FROM_35000_TO_45000', - label: '35,000-45,000', - color: 'orange', - position: 3, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .from45000To55000, - value: 'FROM_45000_TO_55000', - label: '45,000-55,000', - color: 'purple', - position: 4, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .from55000To70000, - value: 'FROM_55000_TO_70000', - label: '55,000-70,000', - color: 'yellow', - position: 5, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .from70000To85000, - value: 'FROM_70000_TO_85000', - label: '70,000-85,000', - color: 'pink', - position: 6, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .from85000To100000, - value: 'FROM_85000_TO_100000', - label: '85,000-100,000', - color: 'cyan', - position: 7, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .from100000To150000, - value: 'FROM_100000_TO_150000', - label: '100,000-150,000', - color: 'brown', - position: 8, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .from150000To250000, - value: 'FROM_150000_TO_250000', - label: '150,000-250,000', - color: 'lime', - position: 9, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary - .over250000, - value: 'OVER_250000', - label: '>250,000', - color: 'violet', - position: 10, - }, - ], + options: buildSelectOptions({ + meta: INFERRED_SALARY_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personInferredSalary, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-interests.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-interests.field.ts index 4aa4f8c815afa..e019a32848a7c 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-interests.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-interests.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlInterests', label: 'Interests', description: 'Interests returned by People Data Labs.', + icon: 'IconHeart', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-id.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-id.field.ts deleted file mode 100644 index 70fdbe6acdb24..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-id.field.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - defineField, - FieldType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineField({ - universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanyId, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - type: FieldType.TEXT, - name: 'pdlJobCompanyId', - label: 'Current Company PDL ID', - description: 'Current company PDL identifier returned by People Data Labs.', - isNullable: true, -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-industry.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-industry.field.ts deleted file mode 100644 index 68a594bc8af8d..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-industry.field.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - defineField, - FieldType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { INDUSTRY_OPTIONS } from 'src/constants/industry-options'; -import { - PDL_FIELD_UNIVERSAL_IDENTIFIERS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, -} from 'src/constants/universal-identifiers'; -import { buildSelectOptions } from 'src/utils/build-select-options'; - -export default defineField({ - universalIdentifier: - PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanyIndustry, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - type: FieldType.SELECT, - name: 'pdlJobCompanyIndustry', - label: 'Current Company Industry', - description: - 'Current company canonical industry returned by People Data Labs.', - isNullable: true, - options: buildSelectOptions( - INDUSTRY_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobCompanyIndustry, - ), -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-linkedin-url.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-linkedin-url.field.ts deleted file mode 100644 index e1d936ebdf075..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-linkedin-url.field.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - defineField, - FieldType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineField({ - universalIdentifier: - PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanyLinkedinUrl, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - type: FieldType.LINKS, - name: 'pdlJobCompanyLinkedinUrl', - label: 'Current Company LinkedIn', - description: 'Current company LinkedIn returned by People Data Labs.', - isNullable: true, -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-size.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-size.field.ts deleted file mode 100644 index 948aa8f233082..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-size.field.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - defineField, - FieldType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { SIZE_OPTIONS } from 'src/constants/size-options'; -import { - PDL_FIELD_UNIVERSAL_IDENTIFIERS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, -} from 'src/constants/universal-identifiers'; -import { buildSelectOptions } from 'src/utils/build-select-options'; - -export default defineField({ - universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanySize, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - type: FieldType.SELECT, - name: 'pdlJobCompanySize', - label: 'Current Company Size', - description: 'Current company size range returned by People Data Labs.', - isNullable: true, - options: buildSelectOptions( - SIZE_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobCompanySize, - ), -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-website.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-website.field.ts deleted file mode 100644 index 6c0fc0b0fd5e5..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-company-website.field.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - defineField, - FieldType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineField({ - universalIdentifier: - PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobCompanyWebsite, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - type: FieldType.LINKS, - name: 'pdlJobCompanyWebsite', - label: 'Current Company Website', - description: 'Current company website returned by People Data Labs.', - isNullable: true, -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-history.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-history.field.ts deleted file mode 100644 index 4c007d2634930..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-history.field.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - defineField, - FieldType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineField({ - universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobHistory, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - type: FieldType.RAW_JSON, - name: 'pdlJobHistory', - label: 'Job History', - description: 'Job history returned by People Data Labs.', - isNullable: true, -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-onet-code.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-onet-code.field.ts index 15fec6cadbb59..f7d7c6513711e 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-onet-code.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-onet-code.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlJobOnetCode', label: 'O*NET Code', description: 'O*NET occupation code returned by People Data Labs.', + icon: 'IconBarcode', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-role.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-role.field.ts index a76744dca9891..5a642e5d3fea5 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-role.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-role.field.ts @@ -4,10 +4,12 @@ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; +import { JOB_ROLE_OPTIONS } from 'src/constants/job-role-options'; import { PDL_FIELD_UNIVERSAL_IDENTIFIERS, PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, } from 'src/constants/universal-identifiers'; +import { buildSelectOptions } from 'src/utils/build-select-options'; export default defineField({ universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobRole, @@ -17,175 +19,10 @@ export default defineField({ name: 'pdlJobRole', label: 'Job Role', description: 'People Data Labs canonical job title role.', + icon: 'IconBriefcase', isNullable: true, - options: [ - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.advisory, - value: 'ADVISORY', - label: 'Advisory', - color: 'blue', - position: 0, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.analyst, - value: 'ANALYST', - label: 'Analyst', - color: 'red', - position: 1, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.creative, - value: 'CREATIVE', - label: 'Creative', - color: 'green', - position: 2, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.education, - value: 'EDUCATION', - label: 'Education', - color: 'orange', - position: 3, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.engineering, - value: 'ENGINEERING', - label: 'Engineering', - color: 'purple', - position: 4, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.finance, - value: 'FINANCE', - label: 'Finance', - color: 'yellow', - position: 5, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.fulfillment, - value: 'FULFILLMENT', - label: 'Fulfillment', - color: 'pink', - position: 6, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.health, - value: 'HEALTH', - label: 'Health', - color: 'cyan', - position: 7, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.hospitality, - value: 'HOSPITALITY', - label: 'Hospitality', - color: 'brown', - position: 8, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.humanResources, - value: 'HUMAN_RESOURCES', - label: 'Human Resources', - color: 'lime', - position: 9, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.legal, - value: 'LEGAL', - label: 'Legal', - color: 'violet', - position: 10, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.manufacturing, - value: 'MANUFACTURING', - label: 'Manufacturing', - color: 'gold', - position: 11, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.marketing, - value: 'MARKETING', - label: 'Marketing', - color: 'turquoise', - position: 12, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.operations, - value: 'OPERATIONS', - label: 'Operations', - color: 'crimson', - position: 13, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.partnerships, - value: 'PARTNERSHIPS', - label: 'Partnerships', - color: 'sky', - position: 14, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.product, - value: 'PRODUCT', - label: 'Product', - color: 'amber', - position: 15, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.professionalService, - value: 'PROFESSIONAL_SERVICE', - label: 'Professional Service', - color: 'plum', - position: 16, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.publicService, - value: 'PUBLIC_SERVICE', - label: 'Public Service', - color: 'grass', - position: 17, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.research, - value: 'RESEARCH', - label: 'Research', - color: 'tomato', - position: 18, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.sales, - value: 'SALES', - label: 'Sales', - color: 'iris', - position: 19, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.salesEngineering, - value: 'SALES_ENGINEERING', - label: 'Sales Engineering', - color: 'mint', - position: 20, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.support, - value: 'SUPPORT', - label: 'Support', - color: 'ruby', - position: 21, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.trade, - value: 'TRADE', - label: 'Trade', - color: 'bronze', - position: 22, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole.unemployed, - value: 'UNEMPLOYED', - label: 'Unemployed', - color: 'jade', - position: 23, - }, - ], + options: buildSelectOptions({ + meta: JOB_ROLE_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.jobRole, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-start-date.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-start-date.field.ts index 937d799c79aad..3a8cc521e9ec8 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-start-date.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-start-date.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlJobStartDate', label: 'Job Start Date', description: 'Current job start date returned by People Data Labs.', + icon: 'IconCalendarTime', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-summary.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-summary.field.ts index 665f88e15b2c1..ac491646adb6f 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-summary.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-summary.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlJobSummary', label: 'Job Summary', description: 'Current job summary returned by People Data Labs.', + icon: 'IconFileDescription', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-title-class.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-title-class.field.ts index 417f81957904c..86db1b9172c92 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-title-class.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-title-class.field.ts @@ -4,10 +4,12 @@ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; +import { JOB_TITLE_CLASS_OPTIONS } from 'src/constants/job-title-class-options'; import { PDL_FIELD_UNIVERSAL_IDENTIFIERS, PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, } from 'src/constants/universal-identifiers'; +import { buildSelectOptions } from 'src/utils/build-select-options'; export default defineField({ universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobTitleClass, @@ -17,45 +19,10 @@ export default defineField({ name: 'pdlJobTitleClass', label: 'Job Class', description: 'People Data Labs canonical job class.', + icon: 'IconCategory', isNullable: true, - options: [ - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobClass - .generalAndAdministrative, - value: 'GENERAL_AND_ADMINISTRATIVE', - label: 'General and Administrative', - color: 'blue', - position: 0, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobClass - .researchAndDevelopment, - value: 'RESEARCH_AND_DEVELOPMENT', - label: 'Research and Development', - color: 'red', - position: 1, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobClass - .salesAndMarketing, - value: 'SALES_AND_MARKETING', - label: 'Sales and Marketing', - color: 'green', - position: 2, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobClass.services, - value: 'SERVICES', - label: 'Services', - color: 'orange', - position: 3, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobClass.unemployed, - value: 'UNEMPLOYED', - label: 'Unemployed', - color: 'purple', - position: 4, - }, - ], + options: buildSelectOptions({ + meta: JOB_TITLE_CLASS_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobClass, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-title-sub-role.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-title-sub-role.field.ts index 6de8cf46df612..b14829e803fac 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-title-sub-role.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-job-title-sub-role.field.ts @@ -4,10 +4,12 @@ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; +import { JOB_TITLE_SUB_ROLE_OPTIONS } from 'src/constants/job-title-sub-role-options'; import { PDL_FIELD_UNIVERSAL_IDENTIFIERS, PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, } from 'src/constants/universal-identifiers'; +import { buildSelectOptions } from 'src/utils/build-select-options'; export default defineField({ universalIdentifier: @@ -18,786 +20,10 @@ export default defineField({ name: 'pdlJobTitleSubRole', label: 'Job Sub-Role', description: 'People Data Labs canonical job sub-role.', + icon: 'IconSubtask', isNullable: true, - options: [ - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.academic, - value: 'ACADEMIC', - label: 'Academic', - color: 'blue', - position: 0, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .accountExecutive, - value: 'ACCOUNT_EXECUTIVE', - label: 'Account Executive', - color: 'red', - position: 1, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .accountManagement, - value: 'ACCOUNT_MANAGEMENT', - label: 'Account Management', - color: 'green', - position: 2, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.accounting, - value: 'ACCOUNTING', - label: 'Accounting', - color: 'orange', - position: 3, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .accountingServices, - value: 'ACCOUNTING_SERVICES', - label: 'Accounting Services', - color: 'purple', - position: 4, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .administrative, - value: 'ADMINISTRATIVE', - label: 'Administrative', - color: 'yellow', - position: 5, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.advisor, - value: 'ADVISOR', - label: 'Advisor', - color: 'pink', - position: 6, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.agriculture, - value: 'AGRICULTURE', - label: 'Agriculture', - color: 'cyan', - position: 7, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.aides, - value: 'AIDES', - label: 'Aides', - color: 'brown', - position: 8, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.architecture, - value: 'ARCHITECTURE', - label: 'Architecture', - color: 'lime', - position: 9, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.artist, - value: 'ARTIST', - label: 'Artist', - color: 'violet', - position: 10, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.boardMember, - value: 'BOARD_MEMBER', - label: 'Board Member', - color: 'gold', - position: 11, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.bookkeeping, - value: 'BOOKKEEPING', - label: 'Bookkeeping', - color: 'turquoise', - position: 12, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.brand, - value: 'BRAND', - label: 'Brand', - color: 'crimson', - position: 13, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .buildingAndGrounds, - value: 'BUILDING_AND_GROUNDS', - label: 'Building and Grounds', - color: 'sky', - position: 14, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .businessAnalyst, - value: 'BUSINESS_ANALYST', - label: 'Business Analyst', - color: 'amber', - position: 15, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .businessDevelopment, - value: 'BUSINESS_DEVELOPMENT', - label: 'Business Development', - color: 'plum', - position: 16, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.chemical, - value: 'CHEMICAL', - label: 'Chemical', - color: 'grass', - position: 17, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.compliance, - value: 'COMPLIANCE', - label: 'Compliance', - color: 'tomato', - position: 18, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.construction, - value: 'CONSTRUCTION', - label: 'Construction', - color: 'iris', - position: 19, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.consulting, - value: 'CONSULTING', - label: 'Consulting', - color: 'mint', - position: 20, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.content, - value: 'CONTENT', - label: 'Content', - color: 'ruby', - position: 21, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .corporateDevelopment, - value: 'CORPORATE_DEVELOPMENT', - label: 'Corporate Development', - color: 'bronze', - position: 22, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.curation, - value: 'CURATION', - label: 'Curation', - color: 'jade', - position: 23, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .customerSuccess, - value: 'CUSTOMER_SUCCESS', - label: 'Customer Success', - color: 'gray', - position: 24, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .customerSupport, - value: 'CUSTOMER_SUPPORT', - label: 'Customer Support', - color: 'blue', - position: 25, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.dataAnalyst, - value: 'DATA_ANALYST', - label: 'Data Analyst', - color: 'red', - position: 26, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .dataEngineering, - value: 'DATA_ENGINEERING', - label: 'Data Engineering', - color: 'green', - position: 27, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.dataScience, - value: 'DATA_SCIENCE', - label: 'Data Science', - color: 'orange', - position: 28, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.dental, - value: 'DENTAL', - label: 'Dental', - color: 'purple', - position: 29, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.devops, - value: 'DEVOPS', - label: 'DevOps', - color: 'yellow', - position: 30, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.doctor, - value: 'DOCTOR', - label: 'Doctor', - color: 'pink', - position: 31, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.electric, - value: 'ELECTRIC', - label: 'Electric', - color: 'cyan', - position: 32, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.electrical, - value: 'ELECTRICAL', - label: 'Electrical', - color: 'brown', - position: 33, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .emergencyServices, - value: 'EMERGENCY_SERVICES', - label: 'Emergency Services', - color: 'lime', - position: 34, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .entertainment, - value: 'ENTERTAINMENT', - label: 'Entertainment', - color: 'violet', - position: 35, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.executive, - value: 'EXECUTIVE', - label: 'Executive', - color: 'gold', - position: 36, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.fashion, - value: 'FASHION', - label: 'Fashion', - color: 'turquoise', - position: 37, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.financial, - value: 'FINANCIAL', - label: 'Financial', - color: 'crimson', - position: 38, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.fitness, - value: 'FITNESS', - label: 'Fitness', - color: 'sky', - position: 39, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.fraud, - value: 'FRAUD', - label: 'Fraud', - color: 'amber', - position: 40, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .graphicDesign, - value: 'GRAPHIC_DESIGN', - label: 'Graphic Design', - color: 'plum', - position: 41, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.growth, - value: 'GROWTH', - label: 'Growth', - color: 'grass', - position: 42, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.hairStylist, - value: 'HAIR_STYLIST', - label: 'Hair Stylist', - color: 'tomato', - position: 43, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.hardware, - value: 'HARDWARE', - label: 'Hardware', - color: 'iris', - position: 44, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .healthAndSafety, - value: 'HEALTH_AND_SAFETY', - label: 'Health and Safety', - color: 'mint', - position: 45, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .humanResources, - value: 'HUMAN_RESOURCES', - label: 'Human Resources', - color: 'ruby', - position: 46, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .implementation, - value: 'IMPLEMENTATION', - label: 'Implementation', - color: 'bronze', - position: 47, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.industrial, - value: 'INDUSTRIAL', - label: 'Industrial', - color: 'jade', - position: 48, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .informationTechnology, - value: 'INFORMATION_TECHNOLOGY', - label: 'Information Technology', - color: 'gray', - position: 49, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.insurance, - value: 'INSURANCE', - label: 'Insurance', - color: 'blue', - position: 50, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .investmentBanking, - value: 'INVESTMENT_BANKING', - label: 'Investment Banking', - color: 'red', - position: 51, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.investor, - value: 'INVESTOR', - label: 'Investor', - color: 'green', - position: 52, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .investorRelations, - value: 'INVESTOR_RELATIONS', - label: 'Investor Relations', - color: 'orange', - position: 53, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.journalism, - value: 'JOURNALISM', - label: 'Journalism', - color: 'purple', - position: 54, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.judicial, - value: 'JUDICIAL', - label: 'Judicial', - color: 'yellow', - position: 55, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.legal, - value: 'LEGAL', - label: 'Legal', - color: 'pink', - position: 56, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .legalServices, - value: 'LEGAL_SERVICES', - label: 'Legal Services', - color: 'cyan', - position: 57, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.logistics, - value: 'LOGISTICS', - label: 'Logistics', - color: 'brown', - position: 58, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.machinist, - value: 'MACHINIST', - label: 'Machinist', - color: 'lime', - position: 59, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .marketingDesign, - value: 'MARKETING_DESIGN', - label: 'Marketing Design', - color: 'violet', - position: 60, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .marketingServices, - value: 'MARKETING_SERVICES', - label: 'Marketing Services', - color: 'gold', - position: 61, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.mechanic, - value: 'MECHANIC', - label: 'Mechanic', - color: 'turquoise', - position: 62, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.mechanical, - value: 'MECHANICAL', - label: 'Mechanical', - color: 'crimson', - position: 63, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.military, - value: 'MILITARY', - label: 'Military', - color: 'sky', - position: 64, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.network, - value: 'NETWORK', - label: 'Network', - color: 'amber', - position: 65, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.nursing, - value: 'NURSING', - label: 'Nursing', - color: 'plum', - position: 66, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.partnerships, - value: 'PARTNERSHIPS', - label: 'Partnerships', - color: 'grass', - position: 67, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.pharmacy, - value: 'PHARMACY', - label: 'Pharmacy', - color: 'tomato', - position: 68, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .planningAndAnalysis, - value: 'PLANNING_AND_ANALYSIS', - label: 'Planning and Analysis', - color: 'iris', - position: 69, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.plumbing, - value: 'PLUMBING', - label: 'Plumbing', - color: 'mint', - position: 70, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.political, - value: 'POLITICAL', - label: 'Political', - color: 'ruby', - position: 71, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .primaryAndSecondary, - value: 'PRIMARY_AND_SECONDARY', - label: 'Primary and Secondary', - color: 'bronze', - position: 72, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.procurement, - value: 'PROCUREMENT', - label: 'Procurement', - color: 'jade', - position: 73, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .productDesign, - value: 'PRODUCT_DESIGN', - label: 'Product Design', - color: 'gray', - position: 74, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .productManagement, - value: 'PRODUCT_MANAGEMENT', - label: 'Product Management', - color: 'blue', - position: 75, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.professor, - value: 'PROFESSOR', - label: 'Professor', - color: 'red', - position: 76, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .projectManagement, - value: 'PROJECT_MANAGEMENT', - label: 'Project Management', - color: 'green', - position: 77, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .protectiveService, - value: 'PROTECTIVE_SERVICE', - label: 'Protective Service', - color: 'orange', - position: 78, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .qaEngineering, - value: 'QA_ENGINEERING', - label: 'QA Engineering', - color: 'purple', - position: 79, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .qualityAssurance, - value: 'QUALITY_ASSURANCE', - label: 'Quality Assurance', - color: 'yellow', - position: 80, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.realtor, - value: 'REALTOR', - label: 'Realtor', - color: 'pink', - position: 81, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.recruiting, - value: 'RECRUITING', - label: 'Recruiting', - color: 'cyan', - position: 82, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.restaurants, - value: 'RESTAURANTS', - label: 'Restaurants', - color: 'brown', - position: 83, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.retail, - value: 'RETAIL', - label: 'Retail', - color: 'lime', - position: 84, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .revenueOperations, - value: 'REVENUE_OPERATIONS', - label: 'Revenue Operations', - color: 'violet', - position: 85, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.risk, - value: 'RISK', - label: 'Risk', - color: 'gold', - position: 86, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .salesDevelopment, - value: 'SALES_DEVELOPMENT', - label: 'Sales Development', - color: 'turquoise', - position: 87, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.scientific, - value: 'SCIENTIFIC', - label: 'Scientific', - color: 'crimson', - position: 88, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.security, - value: 'SECURITY', - label: 'Security', - color: 'sky', - position: 89, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .socialService, - value: 'SOCIAL_SERVICE', - label: 'Social Service', - color: 'amber', - position: 90, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.software, - value: 'SOFTWARE', - label: 'Software', - color: 'plum', - position: 91, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .solutionsEngineer, - value: 'SOLUTIONS_ENGINEER', - label: 'Solutions Engineer', - color: 'grass', - position: 92, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.strategy, - value: 'STRATEGY', - label: 'Strategy', - color: 'tomato', - position: 93, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.student, - value: 'STUDENT', - label: 'Student', - color: 'iris', - position: 94, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .talentAnalytics, - value: 'TALENT_ANALYTICS', - label: 'Talent Analytics', - color: 'mint', - position: 95, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.therapy, - value: 'THERAPY', - label: 'Therapy', - color: 'ruby', - position: 96, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole - .tourAndTravel, - value: 'TOUR_AND_TRAVEL', - label: 'Tour and Travel', - color: 'bronze', - position: 97, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.training, - value: 'TRAINING', - label: 'Training', - color: 'jade', - position: 98, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.translation, - value: 'TRANSLATION', - label: 'Translation', - color: 'gray', - position: 99, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.transport, - value: 'TRANSPORT', - label: 'Transport', - color: 'blue', - position: 100, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.unemployed, - value: 'UNEMPLOYED', - label: 'Unemployed', - color: 'red', - position: 101, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.veterinarian, - value: 'VETERINARIAN', - label: 'Veterinarian', - color: 'green', - position: 102, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.warehouse, - value: 'WAREHOUSE', - label: 'Warehouse', - color: 'orange', - position: 103, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.web, - value: 'WEB', - label: 'Web', - color: 'purple', - position: 104, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole.wellness, - value: 'WELLNESS', - label: 'Wellness', - color: 'yellow', - position: 105, - }, - ], + options: buildSelectOptions({ + meta: JOB_TITLE_SUB_ROLE_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobSubRole, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-languages.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-languages.field.ts index ecc38808109b1..c7652e305ff32 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-languages.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-languages.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlLanguages', label: 'Languages', description: 'Languages returned by People Data Labs.', + icon: 'IconLanguage', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-last-enriched-at.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-last-enriched-at.field.ts index aae63a9eaa8a7..e612a153f98bf 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-last-enriched-at.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-last-enriched-at.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlLastEnrichedAt', label: 'Last Enriched At', description: 'Timestamp of the latest People Data Labs enrichment.', + icon: 'IconClock', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-likelihood.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-likelihood.field.ts index e88e1f6f4d1e3..3d47d630dd6a7 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-likelihood.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-likelihood.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlLikelihood', label: 'Match Likelihood', description: 'People Data Labs match confidence score (1-10).', + icon: 'IconGauge', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-linkedin-connections.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-linkedin-connections.field.ts index 66a95697917d2..042335ed73cda 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-linkedin-connections.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-linkedin-connections.field.ts @@ -15,5 +15,6 @@ export default defineField({ name: 'pdlLinkedinConnections', label: 'LinkedIn Connections', description: 'LinkedIn connection count returned by People Data Labs.', + icon: 'IconAffiliate', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-linkedin-username.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-linkedin-username.field.ts index 374b4685bdfde..fc04f40cf4105 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-linkedin-username.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-linkedin-username.field.ts @@ -15,5 +15,6 @@ export default defineField({ name: 'pdlLinkedinUsername', label: 'LinkedIn Username', description: 'LinkedIn username returned by People Data Labs.', + icon: 'IconBrandLinkedin', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-location-metro.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-location-metro.field.ts index bb6772ed62439..4d11844cdd5b2 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-location-metro.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-location-metro.field.ts @@ -19,9 +19,10 @@ export default defineField({ name: 'pdlLocationMetro', label: 'Metro Area', description: 'People Data Labs canonical metro area.', + icon: 'IconMap2', isNullable: true, - options: buildSelectOptions( - METRO_OPTIONS, - PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personLocationMetro, - ), + options: buildSelectOptions({ + meta: METRO_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personLocationMetro, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-location.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-location.field.ts index 9caf8c15a0504..ef60a8e64b05f 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-location.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-location.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlLocation', label: 'Location', description: 'Location returned by People Data Labs.', + icon: 'IconMapPin', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-name-aliases.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-name-aliases.field.ts index 85ec0a4708403..52a968d11f2f9 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-name-aliases.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-name-aliases.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlNameAliases', label: 'Name Aliases', description: 'Name aliases returned by People Data Labs.', + icon: 'IconSignature', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-profiles.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-profiles.field.ts index e19b0035091ae..c3fa60cc78f7b 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-profiles.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-profiles.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlProfiles', label: 'Social Profiles', description: 'All social profiles returned by People Data Labs.', + icon: 'IconUserCircle', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-raw-payload.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-raw-payload.field.ts index 5b972325773cb..8e45c22b2751e 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-raw-payload.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-raw-payload.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlRawPayload', label: 'PDL Raw Payload', description: 'Full People Data Labs person enrichment response.', + icon: 'IconBraces', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-seniority.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-seniority.field.ts index 41d6dc2834366..af7cf63396685 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-seniority.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-seniority.field.ts @@ -4,10 +4,12 @@ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; +import { SENIORITY_OPTIONS } from 'src/constants/seniority-options'; import { PDL_FIELD_UNIVERSAL_IDENTIFIERS, PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, } from 'src/constants/universal-identifiers'; +import { buildSelectOptions } from 'src/utils/build-select-options'; export default defineField({ universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlSeniority, @@ -17,77 +19,10 @@ export default defineField({ name: 'pdlSeniority', label: 'Seniority', description: 'People Data Labs canonical job title levels.', + icon: 'IconStairsUp', isNullable: true, - options: [ - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.cxo, - value: 'CXO', - label: 'CXO', - color: 'blue', - position: 0, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.owner, - value: 'OWNER', - label: 'Owner', - color: 'red', - position: 1, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.vp, - value: 'VP', - label: 'VP', - color: 'green', - position: 2, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.director, - value: 'DIRECTOR', - label: 'Director', - color: 'orange', - position: 3, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.partner, - value: 'PARTNER', - label: 'Partner', - color: 'purple', - position: 4, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.senior, - value: 'SENIOR', - label: 'Senior', - color: 'yellow', - position: 5, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.manager, - value: 'MANAGER', - label: 'Manager', - color: 'pink', - position: 6, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.entry, - value: 'ENTRY', - label: 'Entry', - color: 'cyan', - position: 7, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.training, - value: 'TRAINING', - label: 'Training', - color: 'brown', - position: 8, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority.unpaid, - value: 'UNPAID', - label: 'Unpaid', - color: 'lime', - position: 9, - }, - ], + options: buildSelectOptions({ + meta: SENIORITY_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.seniority, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-sex.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-sex.field.ts index 1b477d2ae9dd8..8da0e1f46ef38 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-sex.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-sex.field.ts @@ -4,10 +4,12 @@ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, } from 'twenty-sdk/define'; +import { SEX_OPTIONS } from 'src/constants/sex-options'; import { PDL_FIELD_UNIVERSAL_IDENTIFIERS, PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS, } from 'src/constants/universal-identifiers'; +import { buildSelectOptions } from 'src/utils/build-select-options'; export default defineField({ universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlSex, @@ -17,21 +19,10 @@ export default defineField({ name: 'pdlSex', label: 'Sex', description: 'Sex returned by People Data Labs.', + icon: 'IconGenderBigender', isNullable: true, - options: [ - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.sex.male, - value: 'MALE', - label: 'Male', - color: 'blue', - position: 0, - }, - { - id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.sex.female, - value: 'FEMALE', - label: 'Female', - color: 'red', - position: 1, - }, - ], + options: buildSelectOptions({ + meta: SEX_OPTIONS, + ids: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.sex, + }), }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-skills.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-skills.field.ts index f912206e5819a..357c4a6409730 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-skills.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-skills.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlSkills', label: 'Skills', description: 'Skills returned by People Data Labs.', + icon: 'IconBulb', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-summary.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-summary.field.ts index 47835d7c488e6..9b85fae1654a3 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-summary.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-summary.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlSummary', label: 'Summary', description: 'Profile summary returned by People Data Labs.', + icon: 'IconFileText', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-twitter-url.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-twitter-url.field.ts index bd839152c2a13..34ea08615f7c3 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-twitter-url.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-twitter-url.field.ts @@ -14,5 +14,6 @@ export default defineField({ name: 'pdlTwitterUrl', label: 'X / Twitter', description: 'X or Twitter profile returned by People Data Labs.', + icon: 'IconBrandX', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-years-experience.field.ts b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-years-experience.field.ts index 054281270b74c..928e061f590d1 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-years-experience.field.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/fields/person/pdl-years-experience.field.ts @@ -16,5 +16,6 @@ export default defineField({ label: 'Years of Experience', description: 'Inferred years of professional experience from People Data Labs.', + icon: 'IconHourglass', isNullable: true, }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/indexes/company-pdl-id.index.ts b/packages/twenty-apps/internal/people-data-labs/src/indexes/company-pdl-id.index.ts deleted file mode 100644 index 174962cec0ba6..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/indexes/company-pdl-id.index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - defineIndex, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineIndex({ - universalIdentifier: 'b030537d-90e1-4b0a-9def-738ab78c36e0', - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, - fields: [ - { - universalIdentifier: 'b6875e91-f80e-4d6c-bbc1-9ea9217def09', - fieldUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlId, - }, - ], -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/indexes/company-pdl-last-enriched-at.index.ts b/packages/twenty-apps/internal/people-data-labs/src/indexes/company-pdl-last-enriched-at.index.ts deleted file mode 100644 index 32526daa34b54..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/indexes/company-pdl-last-enriched-at.index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - defineIndex, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineIndex({ - universalIdentifier: 'bf1c9130-33c7-4eea-b6f2-50333481c7cc', - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, - fields: [ - { - universalIdentifier: 'cb127127-0494-48a9-9784-395654e1d769', - fieldUniversalIdentifier: - PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLastEnrichedAt, - }, - ], -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/indexes/person-pdl-id.index.ts b/packages/twenty-apps/internal/people-data-labs/src/indexes/person-pdl-id.index.ts deleted file mode 100644 index 5d654803a813d..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/indexes/person-pdl-id.index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - defineIndex, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineIndex({ - universalIdentifier: '6ab8b38f-482c-46dc-a463-202fc4307b92', - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - fields: [ - { - universalIdentifier: 'fd792806-29f1-44da-9199-f76d8219c6db', - fieldUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlId, - }, - ], -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/indexes/person-pdl-last-enriched-at.index.ts b/packages/twenty-apps/internal/people-data-labs/src/indexes/person-pdl-last-enriched-at.index.ts deleted file mode 100644 index bf79c8aa852b7..0000000000000 --- a/packages/twenty-apps/internal/people-data-labs/src/indexes/person-pdl-last-enriched-at.index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - defineIndex, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; - -export default defineIndex({ - universalIdentifier: 'c71369f6-5ad3-4bb8-ade8-1c5597044ee7', - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - fields: [ - { - universalIdentifier: '853c9e52-6aa8-499c-a41a-ed8dcf4b6247', - fieldUniversalIdentifier: - PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlLastEnrichedAt, - }, - ], -}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/company-node.mock.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/company-node.mock.ts new file mode 100644 index 0000000000000..a7308d75d8f04 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/company-node.mock.ts @@ -0,0 +1,11 @@ +import { type CompanyNode } from 'src/types/company-node'; + +export const COMPANY_NODE_MOCK: CompanyNode = { + id: 'c1', + name: '', + domainName: { primaryLinkUrl: 'acme.com' }, + linkedinLink: null, + address: null, + pdlId: null, + pdlLastEnrichedAt: null, +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/create-core-api-client-mock.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/create-core-api-client-mock.ts new file mode 100644 index 0000000000000..ce7875a070cc9 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/create-core-api-client-mock.ts @@ -0,0 +1,35 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; +import { vi } from 'vitest'; + +type Resolver = TResult | ((request: unknown) => TResult); + +type CoreApiClientMockOptions = { + queryResult?: Resolver; + mutationResult?: Resolver; + onMutation?: (request: unknown) => void; +}; + +const resolve = ( + resolver: Resolver | undefined, + request: unknown, +): TResult | undefined => + typeof resolver === 'function' + ? (resolver as (request: unknown) => TResult)(request) + : resolver; + +export const createCoreApiClientMock = ({ + queryResult, + mutationResult, + onMutation, +}: CoreApiClientMockOptions = {}): CoreApiClient => { + const query = vi.fn((request: unknown) => + Promise.resolve(resolve(queryResult, request)), + ); + const mutation = vi.fn((request: unknown) => { + onMutation?.(request); + + return Promise.resolve(resolve(mutationResult, request) ?? {}); + }); + + return { query, mutation } as unknown as CoreApiClient; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/pdl-company-data.mock.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/pdl-company-data.mock.ts new file mode 100644 index 0000000000000..b4e8af949be37 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/pdl-company-data.mock.ts @@ -0,0 +1,27 @@ +import { type PdlCompanyData } from 'src/types/pdl-company-data'; + +export const PDL_COMPANY_DATA_MOCK: PdlCompanyData = { + id: 'pdl-company-1', + display_name: 'Acme Corp', + website: 'acme.com', + linkedin_url: 'https://linkedin.com/company/acme', + industry: 'accounting', + type: 'private', + size: '51-200', + employee_count: 120, + founded: 2001, + total_funding_raised: 1234.5, + latest_funding_stage: 'series_a', + funding_stages: ['seed', 'series_a', 'not-a-stage'], + last_funding_date: '2020-05', + number_funding_rounds: 3, + tags: ['saas', 'b2b'], + naics: [{ naics_code: '541511' }], + location: { + locality: 'San Francisco', + region: 'California', + postal_code: '94107', + country: 'United States', + continent: 'north america', + }, +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/pdl-person-data.mock.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/pdl-person-data.mock.ts new file mode 100644 index 0000000000000..14826c9063631 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/pdl-person-data.mock.ts @@ -0,0 +1,23 @@ +import { type PdlPersonData } from 'src/types/pdl-person-data'; + +export const PDL_PERSON_DATA_MOCK: PdlPersonData = { + id: 'pdl-person-1', + first_name: 'Jane', + last_name: 'Doe', + work_email: 'jane.doe@acme.com', + personal_emails: ['jane@personal.com'], + job_title: 'Chief Executive Officer', + linkedin_url: 'https://linkedin.com/in/janedoe', + industry: 'accounting', + job_title_role: 'engineering', + job_title_levels: ['cxo', 'not-a-level'], + inferred_salary: '45,000-55,000', + job_company_size: '11-50', + skills: ['leadership', 'strategy', 'leadership'], + experience: [{ company: { name: 'Acme' } }], + birth_date: '1990', + location_locality: 'San Francisco', + location_region: 'California', + location_postal_code: '94107', + location_country: 'United States', +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/person-node.mock.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/person-node.mock.ts new file mode 100644 index 0000000000000..8430ebf0c70ee --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/__mocks__/person-node.mock.ts @@ -0,0 +1,12 @@ +import { type PersonNode } from 'src/types/person-node'; + +export const PERSON_NODE_MOCK: PersonNode = { + id: 'p1', + name: { firstName: '', lastName: '' }, + emails: null, + phones: null, + jobTitle: '', + linkedinLink: { primaryLinkUrl: 'https://linkedin.com/in/existing' }, + pdlId: null, + pdlLastEnrichedAt: null, +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-companies.function.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-companies.function.ts new file mode 100644 index 0000000000000..d5274fb839baa --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-companies.function.ts @@ -0,0 +1,48 @@ +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 { type BulkEnrichInput } from 'src/types/bulk-enrich-input'; + +const handler = (input: BulkEnrichInput) => enrichCompaniesCore({ input }); + +export default defineLogicFunction({ + universalIdentifier: PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichCompanies, + name: 'enrich-companies', + description: 'Enrich one or more Company records with People Data Labs data', + timeoutSeconds: 300, + handler, + workflowActionTriggerSettings: { + label: 'Enrich Companies', + icon: 'IconSparkles', + inputSchema: [ + { + type: 'object', + properties: { + records: { + type: 'array', + items: { type: 'object' }, + label: 'Records', + }, + overrideExistingValues: { + type: 'boolean', + label: 'Override Existing Values', + }, + }, + }, + ], + outputSchema: [ + { + type: 'object', + properties: { + success: { type: 'boolean', label: 'Success' }, + total: { type: 'number', label: 'Total' }, + matched: { type: 'number', label: 'Matched' }, + notFound: { type: 'number', label: 'Not Found' }, + skipped: { type: 'number', label: 'Skipped' }, + errored: { type: 'number', label: 'Errored' }, + }, + }, + ], + }, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-company-tool.function.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-company-tool.function.ts new file mode 100644 index 0000000000000..0105ef05a22db --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-company-tool.function.ts @@ -0,0 +1,52 @@ +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, + }, + }, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-people.function.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-people.function.ts new file mode 100644 index 0000000000000..1eba8c7815f9e --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-people.function.ts @@ -0,0 +1,48 @@ +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 { type BulkEnrichInput } from 'src/types/bulk-enrich-input'; + +const handler = (input: BulkEnrichInput) => enrichPeopleCore({ input }); + +export default defineLogicFunction({ + universalIdentifier: PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichPeople, + name: 'enrich-people', + description: 'Enrich one or more Person records with People Data Labs data', + timeoutSeconds: 300, + handler, + workflowActionTriggerSettings: { + label: 'Enrich People', + icon: 'IconSparkles', + inputSchema: [ + { + type: 'object', + properties: { + records: { + type: 'array', + items: { type: 'object' }, + label: 'Records', + }, + overrideExistingValues: { + type: 'boolean', + label: 'Override Existing Values', + }, + }, + }, + ], + outputSchema: [ + { + type: 'object', + properties: { + success: { type: 'boolean', label: 'Success' }, + total: { type: 'number', label: 'Total' }, + matched: { type: 'number', label: 'Matched' }, + notFound: { type: 'number', label: 'Not Found' }, + skipped: { type: 'number', label: 'Skipped' }, + errored: { type: 'number', label: 'Errored' }, + }, + }, + ], + }, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-person-tool.function.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-person-tool.function.ts new file mode 100644 index 0000000000000..2b8e8cfd0d7c2 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/enrich-person-tool.function.ts @@ -0,0 +1,50 @@ +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, + }, + }, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-config-error.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-config-error.spec.ts new file mode 100644 index 0000000000000..4f810b1c6edfd --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-config-error.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { PdlConfigError } from 'src/logic-functions/errors/pdl-config-error'; +import { PdlError } from 'src/logic-functions/errors/pdl-error'; + +describe('PdlConfigError', () => { + it('is a PdlError with a dedicated name, code, and the given message', () => { + const error = new PdlConfigError('missing key'); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(PdlError); + expect(error.name).toBe('PdlConfigError'); + expect(error.code).toBe('CONFIGURATION'); + expect(error.message).toBe('missing key'); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-invalid-input-error.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-invalid-input-error.spec.ts new file mode 100644 index 0000000000000..9f17ff406d4b6 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-invalid-input-error.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { PdlError } from 'src/logic-functions/errors/pdl-error'; +import { PdlInvalidInputError } from 'src/logic-functions/errors/pdl-invalid-input-error'; + +describe('PdlInvalidInputError', () => { + it('is a PdlError with a dedicated name, code, and the given message', () => { + const error = new PdlInvalidInputError('recordId is required'); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(PdlError); + expect(error.name).toBe('PdlInvalidInputError'); + expect(error.code).toBe('INVALID_INPUT'); + expect(error.message).toBe('recordId is required'); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-operation-error.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-operation-error.spec.ts new file mode 100644 index 0000000000000..6e833c3d9f96f --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-operation-error.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { PdlError } from 'src/logic-functions/errors/pdl-error'; +import { PdlOperationError } from 'src/logic-functions/errors/pdl-operation-error'; + +describe('PdlOperationError', () => { + it('is a PdlError with a dedicated name, code, and the given message', () => { + const error = new PdlOperationError('no id returned'); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(PdlError); + expect(error.name).toBe('PdlOperationError'); + expect(error.code).toBe('OPERATION_FAILED'); + expect(error.message).toBe('no id returned'); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-record-not-found-error.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-record-not-found-error.spec.ts new file mode 100644 index 0000000000000..bc629d6049c7b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/__tests__/pdl-record-not-found-error.spec.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; + +import { PdlError } from 'src/logic-functions/errors/pdl-error'; +import { PdlRecordNotFoundError } from 'src/logic-functions/errors/pdl-record-not-found-error'; + +describe('PdlRecordNotFoundError', () => { + it('exposes the object type and record id alongside a built message', () => { + const error = new PdlRecordNotFoundError({ + objectNameSingular: 'Person', + recordId: 'p1', + }); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(PdlError); + expect(error.name).toBe('PdlRecordNotFoundError'); + expect(error.code).toBe('RECORD_NOT_FOUND'); + expect(error.objectNameSingular).toBe('Person'); + expect(error.recordId).toBe('p1'); + expect(error.message).toBe('Person p1 not found'); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-config-error.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-config-error.ts new file mode 100644 index 0000000000000..642cb98b36e74 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-config-error.ts @@ -0,0 +1,8 @@ +import { PdlError } from 'src/logic-functions/errors/pdl-error'; +import { PdlErrorCode } from 'src/logic-functions/errors/pdl-error-code'; + +export class PdlConfigError extends PdlError { + constructor(message: string) { + super({ message, code: PdlErrorCode.CONFIGURATION }); + } +} diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-error-code.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-error-code.ts new file mode 100644 index 0000000000000..1f6fb70ce909e --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-error-code.ts @@ -0,0 +1,8 @@ +export const PdlErrorCode = { + CONFIGURATION: 'CONFIGURATION', + INVALID_INPUT: 'INVALID_INPUT', + RECORD_NOT_FOUND: 'RECORD_NOT_FOUND', + OPERATION_FAILED: 'OPERATION_FAILED', +} as const; + +export type PdlErrorCode = (typeof PdlErrorCode)[keyof typeof PdlErrorCode]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-error.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-error.ts new file mode 100644 index 0000000000000..e1b132d0e6dda --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-error.ts @@ -0,0 +1,12 @@ +import { type PdlErrorCode } from 'src/logic-functions/errors/pdl-error-code'; + +export abstract class PdlError extends Error { + readonly code: PdlErrorCode; + + constructor({ message, code }: { message: string; code: PdlErrorCode }) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = new.target.name; + this.code = code; + } +} diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-invalid-input-error.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-invalid-input-error.ts new file mode 100644 index 0000000000000..4ab18bdcb2a50 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-invalid-input-error.ts @@ -0,0 +1,8 @@ +import { PdlError } from 'src/logic-functions/errors/pdl-error'; +import { PdlErrorCode } from 'src/logic-functions/errors/pdl-error-code'; + +export class PdlInvalidInputError extends PdlError { + constructor(message: string) { + super({ message, code: PdlErrorCode.INVALID_INPUT }); + } +} diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-operation-error.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-operation-error.ts new file mode 100644 index 0000000000000..3227a9216ccb0 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-operation-error.ts @@ -0,0 +1,8 @@ +import { PdlError } from 'src/logic-functions/errors/pdl-error'; +import { PdlErrorCode } from 'src/logic-functions/errors/pdl-error-code'; + +export class PdlOperationError extends PdlError { + constructor(message: string) { + super({ message, code: PdlErrorCode.OPERATION_FAILED }); + } +} diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-record-not-found-error.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-record-not-found-error.ts new file mode 100644 index 0000000000000..69fb8e6578338 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/errors/pdl-record-not-found-error.ts @@ -0,0 +1,22 @@ +import { PdlError } from 'src/logic-functions/errors/pdl-error'; +import { PdlErrorCode } from 'src/logic-functions/errors/pdl-error-code'; + +export class PdlRecordNotFoundError extends PdlError { + readonly objectNameSingular: string; + readonly recordId: string; + + constructor({ + objectNameSingular, + recordId, + }: { + objectNameSingular: string; + recordId: string; + }) { + super({ + message: `${objectNameSingular} ${recordId} not found`, + code: PdlErrorCode.RECORD_NOT_FOUND, + }); + this.objectNameSingular = objectNameSingular; + this.recordId = recordId; + } +} diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/__tests__/enrich-companies.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/__tests__/enrich-companies.spec.ts new file mode 100644 index 0000000000000..d0b5093bce88a --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/__tests__/enrich-companies.spec.ts @@ -0,0 +1,159 @@ +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 { enrichCompaniesCore } from 'src/logic-functions/handlers/enrich-companies'; +import { enrichCompanies } from 'src/logic-functions/utils/enrich-companies'; +import { type CompanyNode } from 'src/types/company-node'; + +vi.mock('src/logic-functions/utils/enrich-companies', () => ({ + enrichCompanies: vi.fn(), +})); + +const enrichCompaniesMock = vi.mocked(enrichCompanies); + +type Captured = { + updateCompany?: Record; + updateCompanies?: { filter: unknown; data: Record }; +}; + +type MutationRequest = { + updateCompany?: { __args: { id: string; data: Record } }; + updateCompanies?: { __args: { filter: unknown; data: Record } }; +}; + +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') => + enrichCompaniesCore({ input: { records: [{ id: recordId }] }, client }); + +beforeEach(() => { + enrichCompaniesMock.mockReset(); +}); + +describe('enrichCompaniesCore', () => { + it('fills empty standard fields and writes pdl metadata via updateCompany on a match', async () => { + enrichCompaniesMock.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(enrichCompaniesMock).toHaveBeenCalledTimes(1); + expect(result.matched).toBe(1); + expect(captured.updateCompany?.name).toBe('Acme Corp'); + expect(captured.updateCompany?.pdlIndustry).toBe('ACCOUNTING'); + expect(captured.updateCompany?.pdlEnrichmentStatus).toBe('MATCHED'); + expect(typeof captured.updateCompany?.pdlLastEnrichedAt).toBe('string'); + expect('domainName' in (captured.updateCompany ?? {})).toBe(false); + expect('pdlLikelihood' in (captured.updateCompany ?? {})).toBe(false); + }); + + it('records NOT_FOUND and writes the status via a batched updateCompanies', async () => { + enrichCompaniesMock.mockResolvedValue([{ outcome: 'not_found', httpStatus: 404 }]); + const captured: Captured = {}; + const client = buildClient([COMPANY_NODE_MOCK], captured); + + const result = await runOne(client); + + expect(result.notFound).toBe(1); + expect(captured.updateCompany).toBeUndefined(); + expect(Object.keys(captured.updateCompanies?.data ?? {}).sort()).toEqual([ + 'pdlEnrichmentStatus', + 'pdlLastEnrichedAt', + ]); + expect(captured.updateCompanies?.filter).toEqual({ id: { in: ['c1'] } }); + }); + + it('records ERROR and reports failure on a PDL error', async () => { + enrichCompaniesMock.mockResolvedValue([ + { outcome: 'error', httpStatus: 500, message: 'boom' }, + ]); + const captured: Captured = {}; + const client = buildClient([COMPANY_NODE_MOCK], captured); + + const result = await runOne(client); + + expect(result.errored).toBe(1); + expect(result.success).toBe(false); + expect(result.results[0].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.skipped).toBe(1); + expect(enrichCompaniesMock).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.errored).toBe(1); + expect(result.results[0].error).toBe('Company missing not found'); + expect(enrichCompaniesMock).not.toHaveBeenCalled(); + }); + + it('batches the PDL request and the not-found status write across records', async () => { + enrichCompaniesMock.mockResolvedValue([ + { outcome: 'not_found', httpStatus: 404 }, + { outcome: 'not_found', httpStatus: 404 }, + ]); + const captured: Captured = {}; + const client = buildClient( + [COMPANY_NODE_MOCK, { ...COMPANY_NODE_MOCK, id: 'c2' }], + captured, + ); + + const result = await enrichCompaniesCore({ + input: { records: [{ id: 'c1' }, { id: 'c2' }] }, + client, + }); + + expect(enrichCompaniesMock).toHaveBeenCalledTimes(1); + expect(captured.updateCompanies?.filter).toEqual({ id: { in: ['c1', 'c2'] } }); + expect(result.notFound).toBe(2); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/__tests__/enrich-people.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/__tests__/enrich-people.spec.ts new file mode 100644 index 0000000000000..39f777705df14 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/__tests__/enrich-people.spec.ts @@ -0,0 +1,284 @@ +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 { enrichPeopleCore } from 'src/logic-functions/handlers/enrich-people'; +import { enrichPeople } from 'src/logic-functions/utils/enrich-people'; +import { type PersonNode } from 'src/types/person-node'; + +vi.mock('src/logic-functions/utils/enrich-people', () => ({ + enrichPeople: vi.fn(), +})); + +const enrichPeopleMock = vi.mocked(enrichPeople); + +type Captured = { + updatePerson?: Record; + updatePeople?: { filter: unknown; data: Record }; + companiesQueried?: boolean; + createCompanyCalled?: boolean; +}; + +type MutationRequest = { + updatePerson?: { __args: { id: string; data: Record } }; + updatePeople?: { __args: { filter: unknown; data: Record } }; + createCompany?: 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, + }; + } + if ('createCompany' in mutation) { + captured.createCompanyCalled = true; + } +}; + +const buildClient = (options: { + people: PersonNode[]; + captured: Captured; + createCompanyId?: string; +}): CoreApiClient => + createCoreApiClientMock({ + queryResult: (request: unknown) => { + if ('companies' in (request as object)) { + options.captured.companiesQueried = true; + + return { companies: { edges: [] } }; + } + + return { people: { edges: options.people.map((node) => ({ node })) } }; + }, + mutationResult: (request: unknown) => + 'createCompany' in (request as object) && + options.createCompanyId !== undefined + ? { createCompany: { id: options.createCompanyId } } + : {}, + onMutation: captureMutation(options.captured), + }); + +const runOne = (client: CoreApiClient, recordId = 'p1') => + enrichPeopleCore({ input: { records: [{ id: recordId }] }, client }); + +beforeEach(() => { + enrichPeopleMock.mockReset(); +}); + +describe('enrichPeopleCore', () => { + it('fills empty standard fields and writes pdl metadata via updatePerson on a match', async () => { + enrichPeopleMock.mockResolvedValue([ + { + outcome: 'matched', + httpStatus: 200, + likelihood: 8, + data: { + id: 'pdl1', + first_name: 'Jane', + last_name: 'Doe', + work_email: 'jane@acme.com', + job_title: 'CEO', + linkedin_url: 'https://linkedin.com/in/new', + }, + }, + ]); + const captured: Captured = {}; + const client = buildClient({ people: [PERSON_NODE_MOCK], captured }); + + const result = await runOne(client); + + expect(enrichPeopleMock).toHaveBeenCalledTimes(1); + expect(result.matched).toBe(1); + expect(captured.updatePerson?.name).toEqual({ + firstName: 'Jane', + lastName: 'Doe', + }); + expect(captured.updatePerson?.jobTitle).toBe('CEO'); + expect(captured.updatePerson?.emails).toMatchObject({ + primaryEmail: 'jane@acme.com', + }); + expect(captured.updatePerson?.pdlEnrichmentStatus).toBe('MATCHED'); + expect(captured.updatePerson?.pdlLikelihood).toBe(8); + expect(typeof captured.updatePerson?.pdlLastEnrichedAt).toBe('string'); + expect(captured.updatePerson?.pdlRawPayload).toMatchObject({ id: 'pdl1' }); + expect('linkedinLink' in (captured.updatePerson ?? {})).toBe(false); + }); + + it('links a found-or-created company when the person has none', async () => { + enrichPeopleMock.mockResolvedValue([ + { + outcome: 'matched', + httpStatus: 200, + likelihood: 8, + data: { + id: 'pdl1', + work_email: 'jane@acme.com', + job_company_name: 'Acme', + job_company_website: 'acme.com', + }, + }, + ]); + const captured: Captured = {}; + const client = buildClient({ + people: [PERSON_NODE_MOCK], + captured, + createCompanyId: 'co-new', + }); + + await runOne(client); + + expect(captured.updatePerson?.companyId).toBe('co-new'); + }); + + it('does not touch company when the person already has one', async () => { + enrichPeopleMock.mockResolvedValue([ + { + outcome: 'matched', + httpStatus: 200, + likelihood: 8, + data: { id: 'pdl1', work_email: 'jane@acme.com', job_company_name: 'Acme' }, + }, + ]); + const captured: Captured = {}; + const client = buildClient({ + people: [{ ...PERSON_NODE_MOCK, company: { id: 'co-existing' } }], + captured, + }); + + await runOne(client); + + expect(captured.companiesQueried).toBeUndefined(); + expect(captured.createCompanyCalled).toBeUndefined(); + expect('companyId' in (captured.updatePerson ?? {})).toBe(false); + }); + + it('does not overwrite a populated standard field', async () => { + enrichPeopleMock.mockResolvedValue([ + { + outcome: 'matched', + httpStatus: 200, + likelihood: 5, + data: { id: 'pdl1', job_title: 'CEO' }, + }, + ]); + const captured: Captured = {}; + const client = buildClient({ + people: [{ ...PERSON_NODE_MOCK, jobTitle: 'Existing Title' }], + captured, + }); + + await runOne(client); + + expect('jobTitle' in (captured.updatePerson ?? {})).toBe(false); + }); + + it('records NOT_FOUND and writes the status via a batched updatePeople', async () => { + enrichPeopleMock.mockResolvedValue([{ outcome: 'not_found', httpStatus: 404 }]); + const captured: Captured = {}; + const client = buildClient({ people: [PERSON_NODE_MOCK], captured }); + + const result = await runOne(client); + + expect(result.notFound).toBe(1); + expect(captured.updatePerson).toBeUndefined(); + expect(Object.keys(captured.updatePeople?.data ?? {}).sort()).toEqual([ + 'pdlEnrichmentStatus', + 'pdlLastEnrichedAt', + ]); + 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 () => { + enrichPeopleMock.mockResolvedValue([ + { outcome: 'error', httpStatus: 500, message: 'boom' }, + ]); + const captured: Captured = {}; + const client = buildClient({ people: [PERSON_NODE_MOCK], captured }); + + const result = await runOne(client); + + expect(result.errored).toBe(1); + expect(result.success).toBe(false); + expect(result.results[0].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 () => { + enrichPeopleMock.mockResolvedValue([ + { + outcome: 'matched', + httpStatus: 200, + likelihood: 5, + data: { id: 'pdl1', job_title: 'CEO' }, + }, + ]); + const captured: Captured = {}; + const client = buildClient({ + people: [{ ...PERSON_NODE_MOCK, jobTitle: 'Existing Title' }], + captured, + }); + + await enrichPeopleCore({ + input: { records: [{ id: '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({ + people: [{ ...PERSON_NODE_MOCK, linkedinLink: null }], + captured, + }); + + const result = await runOne(client); + + expect(result.skipped).toBe(1); + expect(enrichPeopleMock).not.toHaveBeenCalled(); + }); + + it('marks a missing record as ERROR', async () => { + const captured: Captured = {}; + const client = buildClient({ people: [], captured }); + + const result = await runOne(client, 'missing'); + + expect(result.errored).toBe(1); + expect(result.results[0].error).toBe('Person missing not found'); + expect(enrichPeopleMock).not.toHaveBeenCalled(); + }); + + it('batches the PDL request and the not-found status write across records', async () => { + enrichPeopleMock.mockResolvedValue([ + { outcome: 'not_found', httpStatus: 404 }, + { outcome: 'not_found', httpStatus: 404 }, + ]); + const captured: Captured = {}; + const client = buildClient({ + people: [PERSON_NODE_MOCK, { ...PERSON_NODE_MOCK, id: 'p2' }], + captured, + }); + + const result = await enrichPeopleCore({ + input: { records: [{ id: 'p1' }, { id: 'p2' }] }, + client, + }); + + expect(enrichPeopleMock).toHaveBeenCalledTimes(1); + expect(captured.updatePeople?.filter).toEqual({ id: { in: ['p1', 'p2'] } }); + expect(result.notFound).toBe(2); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/__tests__/post-install.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/__tests__/post-install.spec.ts new file mode 100644 index 0000000000000..091c240f478e8 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/__tests__/post-install.spec.ts @@ -0,0 +1,153 @@ +import { type MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { describe, expect, it, vi } from 'vitest'; + +import { PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; +import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock'; +import { postInstallCore } from 'src/logic-functions/handlers/post-install'; + +type AnyRequest = Record; + +const buildMetadataClient = ( + logicFunctions: { id: string; universalIdentifier: string }[], +): MetadataApiClient => + ({ + query: vi.fn(() => + Promise.resolve({ findManyLogicFunctions: logicFunctions }), + ), + }) as unknown as MetadataApiClient; + +const buildCoreClient = () => + createCoreApiClientMock({ + queryResult: (request: unknown) => { + const req = request as AnyRequest; + if ('workflows' in req) { + return { workflows: { edges: [] } }; + } + if ('workflowVersions' in req) { + return { + workflowVersions: { + edges: [{ node: { id: 'version-1', status: 'DRAFT' } }], + }, + }; + } + return {}; + }, + mutationResult: (request: unknown) => + 'createWorkflow' in (request as AnyRequest) + ? { createWorkflow: { id: 'workflow-new' } } + : {}, + }); + +describe('postInstallCore', () => { + it('seeds a workflow for each resolvable enrich function', async () => { + const metadataClient = buildMetadataClient([ + { + id: 'company-fn', + universalIdentifier: + PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichCompanies, + }, + { + id: 'person-fn', + universalIdentifier: + PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichPeople, + }, + ]); + + const result = await postInstallCore({ + coreClient: buildCoreClient(), + metadataClient, + }); + + expect(result.seededWorkflows.map((workflow) => workflow.objectNameSingular)).toEqual([ + 'company', + 'person', + ]); + expect( + result.seededWorkflows.every((workflow) => workflow.status === 'created'), + ).toBe(true); + }); + + it('skips a workflow when its logic function is not installed', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const metadataClient = buildMetadataClient([ + { + id: 'company-fn', + universalIdentifier: + PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichCompanies, + }, + ]); + + const result = await postInstallCore({ + coreClient: buildCoreClient(), + metadataClient, + }); + + expect(result.seededWorkflows).toHaveLength(1); + expect(result.seededWorkflows[0]?.objectNameSingular).toBe('company'); + expect(warnSpy).toHaveBeenCalledTimes(1); + + warnSpy.mockRestore(); + }); + + it('continues seeding remaining workflows when one seed fails', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const metadataClient = buildMetadataClient([ + { + id: 'company-fn', + universalIdentifier: + PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichCompanies, + }, + { + id: 'person-fn', + universalIdentifier: + PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.enrichPeople, + }, + ]); + + const coreClient = createCoreApiClientMock({ + queryResult: (request: unknown) => { + const req = request as AnyRequest; + if ('workflows' in req) { + return { workflows: { edges: [] } }; + } + if ('workflowVersions' in req) { + return { + workflowVersions: { + edges: [{ node: { id: 'version-1', status: 'DRAFT' } }], + }, + }; + } + return {}; + }, + mutationResult: (request: unknown) => { + const req = request as AnyRequest; + if ('createWorkflow' in req) { + const args = (req.createWorkflow as AnyRequest)?.__args as AnyRequest; + const workflowName = (args?.data as AnyRequest)?.name; + return workflowName === 'Enrich companies with People Data Labs' + ? { createWorkflow: {} } + : { createWorkflow: { id: 'workflow-person' } }; + } + return {}; + }, + }); + + const result = await postInstallCore({ coreClient, metadataClient }); + + expect(result.seededWorkflows).toHaveLength(2); + expect(result.seededWorkflows[0]).toMatchObject({ + objectNameSingular: 'company', + status: 'failed', + }); + expect(result.seededWorkflows[0]?.error).toBeDefined(); + expect(result.seededWorkflows[1]).toMatchObject({ + objectNameSingular: 'person', + status: 'created', + }); + expect(warnSpy).toHaveBeenCalledTimes(1); + + warnSpy.mockRestore(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/company-enrichment-adapter.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/company-enrichment-adapter.ts new file mode 100644 index 0000000000000..c4ce29d9dc3ac --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/company-enrichment-adapter.ts @@ -0,0 +1,33 @@ +import { buildCompanyMatchedData } from 'src/logic-functions/utils/build-company-matched-data'; +import { enrichCompanies } from 'src/logic-functions/utils/enrich-companies'; +import { extractCompanyMatchParams } from 'src/logic-functions/utils/extract-company-match-params'; +import { readCompanies } from 'src/logic-functions/utils/read-companies'; +import { updateCompaniesStatus } from 'src/logic-functions/utils/update-companies-status'; +import { updateCompanyRecord } from 'src/logic-functions/utils/update-company-record'; +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 companyEnrichmentAdapter: BatchEnrichmentAdapter< + CompanyNode, + PdlCompanyData, + PdlCompanyEnrichParams +> = { + objectNameSingular: 'Company', + noIdentifierMessage: + 'No usable identifier (domain, LinkedIn, or name) to match against PDL.', + readRecords: readCompanies, + getNodeId: (node) => node.id, + extractParams: extractCompanyMatchParams, + enrichBatch: enrichCompanies, + buildMatchedData: ({ node, outcome, enrichedAt, overrideExistingValues }) => + buildCompanyMatchedData({ + node, + outcome, + enrichedAt, + overrideExistingValues, + }), + updateOne: updateCompanyRecord, + updateManyStatus: updateCompaniesStatus, +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/enrich-companies.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/enrich-companies.ts new file mode 100644 index 0000000000000..1fe51174c2d74 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/enrich-companies.ts @@ -0,0 +1,15 @@ +import { CoreApiClient } from 'twenty-client-sdk/core'; + +import { companyEnrichmentAdapter } from 'src/logic-functions/handlers/company-enrichment-adapter'; +import { runBatchEnrichment } from 'src/logic-functions/utils/run-batch-enrichment'; +import { type BulkEnrichInput } from 'src/types/bulk-enrich-input'; +import { type BulkEnrichResult } from 'src/types/bulk-enrich-result'; + +export const enrichCompaniesCore = ({ + input, + client = new CoreApiClient(), +}: { + input: BulkEnrichInput; + client?: CoreApiClient; +}): Promise => + runBatchEnrichment({ client, input, adapter: companyEnrichmentAdapter }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/enrich-people.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/enrich-people.ts new file mode 100644 index 0000000000000..1035d5a5a56a8 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/enrich-people.ts @@ -0,0 +1,15 @@ +import { CoreApiClient } from 'twenty-client-sdk/core'; + +import { personEnrichmentAdapter } from 'src/logic-functions/handlers/person-enrichment-adapter'; +import { runBatchEnrichment } from 'src/logic-functions/utils/run-batch-enrichment'; +import { type BulkEnrichInput } from 'src/types/bulk-enrich-input'; +import { type BulkEnrichResult } from 'src/types/bulk-enrich-result'; + +export const enrichPeopleCore = ({ + input, + client = new CoreApiClient(), +}: { + input: BulkEnrichInput; + client?: CoreApiClient; +}): Promise => + runBatchEnrichment({ client, input, adapter: personEnrichmentAdapter }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/person-enrichment-adapter.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/person-enrichment-adapter.ts new file mode 100644 index 0000000000000..3d345e0509b68 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/person-enrichment-adapter.ts @@ -0,0 +1,27 @@ +import { buildPersonMatchedData } from 'src/logic-functions/utils/build-person-matched-data'; +import { enrichPeople } from 'src/logic-functions/utils/enrich-people'; +import { extractPersonMatchParams } from 'src/logic-functions/utils/extract-person-match-params'; +import { readPeople } from 'src/logic-functions/utils/read-people'; +import { updatePeopleStatus } from 'src/logic-functions/utils/update-people-status'; +import { updatePersonRecord } from 'src/logic-functions/utils/update-person-record'; +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 personEnrichmentAdapter: BatchEnrichmentAdapter< + PersonNode, + PdlPersonData, + PdlPersonEnrichParams +> = { + objectNameSingular: 'Person', + noIdentifierMessage: + 'No usable identifier (email, LinkedIn, PDL id, or name paired with a company) to match against PDL.', + readRecords: readPeople, + getNodeId: (node) => node.id, + extractParams: extractPersonMatchParams, + enrichBatch: enrichPeople, + buildMatchedData: buildPersonMatchedData, + updateOne: updatePersonRecord, + updateManyStatus: updatePeopleStatus, +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/post-install.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/post-install.ts new file mode 100644 index 0000000000000..6fd5f3dff2b3d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/handlers/post-install.ts @@ -0,0 +1,69 @@ +import { CoreApiClient } from 'twenty-client-sdk/core'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; + +import { ENRICHMENT_WORKFLOW_SEEDS } from 'src/constants/enrichment-workflow-seeds'; +import { resolveLogicFunctionId } from 'src/logic-functions/utils/resolve-logic-function-id'; +import { seedEnrichmentWorkflow } from 'src/logic-functions/utils/seed-enrichment-workflow'; +import { type PostInstallResult } from 'src/types/post-install-result'; +import { type SeedEnrichmentWorkflowResult } from 'src/types/seed-enrichment-workflow-result'; +import { isDefined } from 'src/utils/is-defined'; + +export const postInstallCore = async ({ + coreClient = new CoreApiClient(), + metadataClient = new MetadataApiClient(), +}: { + coreClient?: CoreApiClient; + metadataClient?: MetadataApiClient; +} = {}): Promise => { + const { findManyLogicFunctions: logicFunctions } = await metadataClient.query( + { + findManyLogicFunctions: { + id: true, + universalIdentifier: true, + }, + }, + ); + + const seededWorkflows: SeedEnrichmentWorkflowResult[] = []; + + for (const seed of ENRICHMENT_WORKFLOW_SEEDS) { + const logicFunctionId = resolveLogicFunctionId({ + logicFunctions, + universalIdentifier: seed.logicFunctionUniversalIdentifier, + }); + + if (!isDefined(logicFunctionId)) { + console.warn( + `[people-data-labs] Skipping "${seed.workflowName}": logic function ${seed.logicFunctionUniversalIdentifier} is not installed.`, + ); + continue; + } + + try { + const result = await seedEnrichmentWorkflow({ + client: coreClient, + logicFunctionId, + seed, + }); + + seededWorkflows.push(result); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + + console.warn( + `[people-data-labs] Failed to seed "${seed.workflowName}": ${errorMessage}`, + error, + ); + + seededWorkflows.push({ + objectNameSingular: seed.objectNameSingular, + workflowName: seed.workflowName, + status: 'failed', + error: errorMessage, + }); + } + } + + return { seededWorkflows }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/post-install.function.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/post-install.function.ts new file mode 100644 index 0000000000000..5b1c1a5b4d552 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/post-install.function.ts @@ -0,0 +1,17 @@ +import { definePostInstallLogicFunction } from 'twenty-sdk/define'; + +import { PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; + +const handler = async () => { + return { seededWorkflows: [] }; +}; + +export default definePostInstallLogicFunction({ + universalIdentifier: PDL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIERS.postInstall, + name: 'post-install', + description: + 'Post-install hook for the People Data Labs app (workflow seeding is not currently wired up).', + timeoutSeconds: 30, + handler, + shouldRunSynchronously: true, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/aggregate-bulk-enrich-result.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/aggregate-bulk-enrich-result.spec.ts new file mode 100644 index 0000000000000..ff9382ccfed92 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/aggregate-bulk-enrich-result.spec.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; + +import { aggregateBulkEnrichResult } from 'src/logic-functions/utils/aggregate-bulk-enrich-result'; +import { type EnrichResult } from 'src/types/enrich-result'; + +const result = (status: EnrichResult['status'], recordId: string): EnrichResult => ({ + success: status !== 'ERROR', + recordId, + status, + updatedFields: [], + message: 'message', +}); + +describe('aggregateBulkEnrichResult', () => { + it('counts each status and flags failure when something errored', () => { + expect( + aggregateBulkEnrichResult([ + result('MATCHED', 'a'), + result('NOT_FOUND', 'b'), + result('SKIPPED', 'c'), + result('ERROR', 'd'), + ]), + ).toMatchObject({ + total: 4, + matched: 1, + notFound: 1, + skipped: 1, + errored: 1, + success: false, + }); + }); + + it('reports success when there are no errors', () => { + expect(aggregateBulkEnrichResult([result('MATCHED', 'a')])).toMatchObject({ + success: true, + errored: 0, + total: 1, + }); + }); + + it('returns an empty summary for no results', () => { + expect(aggregateBulkEnrichResult([])).toEqual({ + success: true, + total: 0, + matched: 0, + notFound: 0, + skipped: 0, + errored: 0, + results: [], + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-address.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-address.spec.ts new file mode 100644 index 0000000000000..c4771e6792656 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-address.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { buildAddress } from 'src/logic-functions/utils/build-address'; + +describe('buildAddress', () => { + it('maps parts to the address composite using addressPostcode', () => { + expect( + buildAddress({ + city: 'San Francisco', + state: 'California', + postcode: '94107', + country: 'United States', + geo: '37.77,-122.41', + }), + ).toEqual({ + addressStreet1: '', + addressStreet2: '', + addressCity: 'San Francisco', + addressPostcode: '94107', + addressState: 'California', + addressCountry: 'United States', + addressLat: 37.77, + addressLng: -122.41, + }); + }); + + it('returns undefined when no part has a value', () => { + expect(buildAddress({ city: '', country: null })).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-allowed-values.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-allowed-values.spec.ts new file mode 100644 index 0000000000000..d1a4bfc31064e --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-allowed-values.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { buildAllowedValues } from 'src/logic-functions/utils/build-allowed-values'; + +describe('buildAllowedValues', () => { + it('builds a set of the option values', () => { + const allowed = buildAllowedValues([ + { key: 'a', value: 'A', label: 'A', color: 'blue', position: 0 }, + { key: 'b', value: 'B', label: 'B', color: 'red', position: 1 }, + ]); + + expect(allowed.has('A')).toBe(true); + expect(allowed.has('B')).toBe(true); + expect(allowed.size).toBe(2); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-bulk-records-trigger.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-bulk-records-trigger.spec.ts new file mode 100644 index 0000000000000..e5234d8e056f1 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-bulk-records-trigger.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { buildBulkRecordsTrigger } from 'src/logic-functions/utils/build-bulk-records-trigger'; + +describe('buildBulkRecordsTrigger', () => { + it('builds a bulk-records manual trigger with no next step wired', () => { + const trigger = buildBulkRecordsTrigger({ + objectNameSingular: 'person', + name: 'When people are selected', + icon: 'IconSparkles', + }); + + expect(trigger).toEqual({ + name: 'When people are selected', + type: 'MANUAL', + settings: { + objectType: 'person', + availability: { type: 'BULK_RECORDS', objectNameSingular: 'person' }, + outputSchema: {}, + icon: 'IconSparkles', + isPinned: false, + }, + nextStepIds: [], + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-company-create-data.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-company-create-data.spec.ts new file mode 100644 index 0000000000000..985acf47897a7 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-company-create-data.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; + +import { buildCompanyCreateData } from 'src/logic-functions/utils/build-company-create-data'; +import { type PdlPersonData } from 'src/types/pdl-person-data'; + +describe('buildCompanyCreateData', () => { + it('maps job_company_* onto Company standard and pdl fields', () => { + const data = buildCompanyCreateData({ + job_company_id: 'pdl-co-1', + job_company_name: 'Acme', + job_company_website: 'acme.com', + job_company_linkedin_url: 'https://linkedin.com/company/acme', + job_company_industry: 'accounting', + job_company_size: '11-50', + } as PdlPersonData); + + expect(data).toMatchObject({ + name: 'Acme', + pdlId: 'pdl-co-1', + pdlIndustry: 'ACCOUNTING', + pdlSizeRange: 'ELEVEN_TO_FIFTY', + }); + expect(data.domainName).toMatchObject({ primaryLinkUrl: 'acme.com' }); + expect(data.linkedinLink).toMatchObject({ + primaryLinkUrl: 'linkedin.com/company/acme', + }); + }); + + it('omits fields PDL did not return and drops unknown select values', () => { + const data = buildCompanyCreateData({ + job_company_name: 'Acme', + job_company_industry: 'not-a-real-industry', + } as PdlPersonData); + + expect(data).toEqual({ name: 'Acme' }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-company-match-keys.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-company-match-keys.spec.ts new file mode 100644 index 0000000000000..ca7b963382905 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-company-match-keys.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; + +import { buildCompanyMatchKeys } from 'src/logic-functions/utils/build-company-match-keys'; +import { type PdlPersonData } from 'src/types/pdl-person-data'; + +describe('buildCompanyMatchKeys', () => { + it('extracts and trims the company identifiers from PDL data', () => { + const keys = buildCompanyMatchKeys({ + job_company_id: ' pdl-co-1 ', + job_company_website: 'acme.com', + job_company_linkedin_url: 'https://linkedin.com/company/acme', + job_company_name: 'Acme', + } as PdlPersonData); + + expect(keys).toEqual({ + pdlId: 'pdl-co-1', + website: 'acme.com', + linkedinUrl: 'linkedin.com/company/acme', + name: 'Acme', + }); + }); + + it('omits identifiers PDL did not return or that are blank', () => { + expect( + buildCompanyMatchKeys({ + job_company_name: ' ', + job_company_website: 'acme.com', + } as PdlPersonData), + ).toEqual({ website: 'acme.com' }); + }); + + it('returns an empty object when no company identifiers are present', () => { + expect(buildCompanyMatchKeys({} as PdlPersonData)).toEqual({}); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-company-matched-data.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-company-matched-data.spec.ts new file mode 100644 index 0000000000000..b6e2024a995a7 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-company-matched-data.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; + +import { COMPANY_NODE_MOCK } from 'src/logic-functions/__mocks__/company-node.mock'; +import { PDL_COMPANY_DATA_MOCK } from 'src/logic-functions/__mocks__/pdl-company-data.mock'; +import { buildCompanyMatchedData } from 'src/logic-functions/utils/build-company-matched-data'; + +const ENRICHED_AT = '2026-01-01T00:00:00.000Z'; + +describe('buildCompanyMatchedData', () => { + it('fills empty standard fields and always writes pdl metadata', async () => { + const data = await buildCompanyMatchedData({ + node: COMPANY_NODE_MOCK, + outcome: { data: PDL_COMPANY_DATA_MOCK }, + enrichedAt: ENRICHED_AT, + overrideExistingValues: false, + }); + + expect(data.name).toBe('Acme Corp'); + expect(data.pdlIndustry).toBe('ACCOUNTING'); + expect(data.pdlEnrichmentStatus).toBe('MATCHED'); + expect(data.pdlLastEnrichedAt).toBe(ENRICHED_AT); + expect('domainName' in data).toBe(false); + }); + + it('overwrites a populated standard field when overrideExistingValues is set', async () => { + const data = await buildCompanyMatchedData({ + node: COMPANY_NODE_MOCK, + outcome: { data: PDL_COMPANY_DATA_MOCK }, + enrichedAt: ENRICHED_AT, + overrideExistingValues: true, + }); + + expect('domainName' in data).toBe(true); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-currency-from-usd.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-currency-from-usd.spec.ts new file mode 100644 index 0000000000000..75fe3bc61927d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-currency-from-usd.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { buildCurrencyFromUsd } from 'src/logic-functions/utils/build-currency-from-usd'; + +describe('buildCurrencyFromUsd', () => { + it('converts a USD amount to micros', () => { + expect(buildCurrencyFromUsd(1234.5)).toEqual({ + amountMicros: 1_234_500_000, + currencyCode: 'USD', + }); + }); + + it('returns undefined for non-finite or non-number input', () => { + expect(buildCurrencyFromUsd(Number.NaN)).toBeUndefined(); + expect(buildCurrencyFromUsd('1000')).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-emails.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-emails.spec.ts new file mode 100644 index 0000000000000..d1cc2db22fffa --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-emails.spec.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { buildEmails } from 'src/logic-functions/utils/build-emails'; + +describe('buildEmails', () => { + it('keeps the first email as primary and the rest as additional', () => { + expect(buildEmails(['work@acme.com', 'home@me.com'])).toEqual({ + primaryEmail: 'work@acme.com', + additionalEmails: ['home@me.com'], + }); + }); + + it('dedupes case-insensitively and skips blanks', () => { + expect(buildEmails(['work@acme.com', 'WORK@acme.com', '', null])).toEqual({ + primaryEmail: 'work@acme.com', + additionalEmails: null, + }); + }); + + it('returns undefined when there are no emails', () => { + expect(buildEmails([null, undefined, ''])).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-error-result.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-error-result.spec.ts new file mode 100644 index 0000000000000..b24df7e0fbe4a --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-error-result.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { buildErrorResult } from 'src/logic-functions/utils/build-error-result'; + +describe('buildErrorResult', () => { + it('builds an ERROR result carrying the error detail', () => { + expect(buildErrorResult({ recordId: 'p1', error: 'boom' })).toEqual({ + success: false, + recordId: 'p1', + status: 'ERROR', + updatedFields: [], + message: 'People Data Labs enrichment failed.', + error: 'boom', + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-full-name.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-full-name.spec.ts new file mode 100644 index 0000000000000..c0b005fcba36d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-full-name.spec.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest'; + +import { buildFullName } from 'src/logic-functions/utils/build-full-name'; + +describe('buildFullName', () => { + it('uses explicit first and last name when present', () => { + expect( + buildFullName({ firstName: 'Jane', lastName: 'Doe', fullName: undefined }), + ).toEqual({ + firstName: 'Jane', + lastName: 'Doe', + }); + }); + + it('fills only the part that is present', () => { + expect( + buildFullName({ firstName: 'Jane', lastName: undefined, fullName: undefined }), + ).toEqual({ + firstName: 'Jane', + lastName: '', + }); + }); + + it('splits a full name on the first whitespace run', () => { + expect( + buildFullName({ + firstName: undefined, + lastName: undefined, + fullName: 'Jane Mary Doe', + }), + ).toEqual({ + firstName: 'Jane', + lastName: 'Mary Doe', + }); + }); + + it('capitalizes lowercase names returned by PDL', () => { + expect( + buildFullName({ firstName: 'sean', lastName: 'thorne', fullName: undefined }), + ).toEqual({ + firstName: 'Sean', + lastName: 'Thorne', + }); + }); + + it('capitalizes a lowercase full name when split', () => { + expect( + buildFullName({ + firstName: undefined, + lastName: undefined, + fullName: 'sean fong thorne', + }), + ).toEqual({ + firstName: 'Sean', + lastName: 'Fong Thorne', + }); + }); + + it('returns undefined when there is no name', () => { + expect( + buildFullName({ firstName: undefined, lastName: undefined, fullName: undefined }), + ).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-links.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-links.spec.ts new file mode 100644 index 0000000000000..659ef641974d9 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-links.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { buildLinks } from 'src/logic-functions/utils/build-links'; + +describe('buildLinks', () => { + it('builds a links value with a blank label by default', () => { + expect(buildLinks({ url: 'https://acme.com' })).toEqual({ + primaryLinkUrl: 'https://acme.com', + primaryLinkLabel: '', + secondaryLinks: null, + }); + }); + + it('uses the provided label', () => { + expect(buildLinks({ url: 'https://acme.com', label: 'Acme' })).toEqual({ + primaryLinkUrl: 'https://acme.com', + primaryLinkLabel: 'Acme', + secondaryLinks: null, + }); + }); + + it('returns undefined when the url is empty', () => { + expect(buildLinks({ url: '' })).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-logic-function-step.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-logic-function-step.spec.ts new file mode 100644 index 0000000000000..934ea29714121 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-logic-function-step.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; + +import { buildLogicFunctionStep } from 'src/logic-functions/utils/build-logic-function-step'; + +describe('buildLogicFunctionStep', () => { + it('builds a logic-function step carrying the resolved id and input', () => { + const step = buildLogicFunctionStep({ + id: 'step-1', + name: 'Enrich with People Data Labs', + logicFunctionId: 'logic-function-1', + logicFunctionInput: { records: '{{trigger.companies}}' }, + }); + + expect(step).toEqual({ + id: 'step-1', + name: 'Enrich with People Data Labs', + type: 'LOGIC_FUNCTION', + valid: true, + settings: { + input: { + logicFunctionId: 'logic-function-1', + logicFunctionInput: { records: '{{trigger.companies}}' }, + }, + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: false }, + continueOnFailure: { value: false }, + }, + }, + nextStepIds: [], + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-matched-result.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-matched-result.spec.ts new file mode 100644 index 0000000000000..f1984c1c27f48 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-matched-result.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { buildMatchedResult } from 'src/logic-functions/utils/build-matched-result'; + +describe('buildMatchedResult', () => { + it('builds a MATCHED result summarizing the written fields', () => { + expect( + buildMatchedResult({ recordId: 'p1', updatedFields: ['name', 'jobTitle'] }), + ).toEqual({ + success: true, + recordId: 'p1', + status: 'MATCHED', + updatedFields: ['name', 'jobTitle'], + message: 'Enriched with People Data Labs (2 fields).', + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-not-found-result.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-not-found-result.spec.ts new file mode 100644 index 0000000000000..a66207fa78303 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-not-found-result.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; + +import { buildNotFoundResult } from 'src/logic-functions/utils/build-not-found-result'; + +describe('buildNotFoundResult', () => { + it('builds a NOT_FOUND result', () => { + expect(buildNotFoundResult('p1')).toEqual({ + success: true, + recordId: 'p1', + status: 'NOT_FOUND', + updatedFields: [], + message: 'People Data Labs returned no confident match.', + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-person-matched-data.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-person-matched-data.spec.ts new file mode 100644 index 0000000000000..a4d2d03895201 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-person-matched-data.spec.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest'; + +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 { buildPersonMatchedData } from 'src/logic-functions/utils/build-person-matched-data'; + +const ENRICHED_AT = '2026-01-01T00:00:00.000Z'; + +describe('buildPersonMatchedData', () => { + it('fills empty fields, writes pdl metadata, and skips company lookup when there is none', async () => { + const client = createCoreApiClientMock(); + + const data = await buildPersonMatchedData({ + client, + node: PERSON_NODE_MOCK, + outcome: { + likelihood: 8, + data: { + id: 'pdl1', + first_name: 'Jane', + last_name: 'Doe', + work_email: 'jane@acme.com', + job_title: 'CEO', + }, + }, + enrichedAt: ENRICHED_AT, + companyIdByMatchKeyCache: new Map(), + overrideExistingValues: false, + }); + + expect(data.name).toEqual({ firstName: 'Jane', lastName: 'Doe' }); + expect(data.jobTitle).toBe('CEO'); + expect(data.pdlLikelihood).toBe(8); + expect(data.pdlEnrichmentStatus).toBe('MATCHED'); + expect(data.pdlLastEnrichedAt).toBe(ENRICHED_AT); + expect('companyId' in data).toBe(false); + }); + + it('overwrites a populated standard field when overrideExistingValues is set', async () => { + const client = createCoreApiClientMock(); + + const data = await buildPersonMatchedData({ + client, + node: { ...PERSON_NODE_MOCK, jobTitle: 'Existing Title' }, + outcome: { likelihood: 8, data: { id: 'pdl1', job_title: 'CEO' } }, + enrichedAt: ENRICHED_AT, + companyIdByMatchKeyCache: new Map(), + overrideExistingValues: true, + }); + + expect(data.jobTitle).toBe('CEO'); + }); + + it('links a found-or-created company when the person has none', async () => { + const client = createCoreApiClientMock({ + queryResult: { companies: { edges: [] } }, + mutationResult: { createCompany: { id: 'co-new' } }, + }); + + const data = await buildPersonMatchedData({ + client, + node: PERSON_NODE_MOCK, + outcome: { + likelihood: 8, + data: { + id: 'pdl1', + work_email: 'jane@acme.com', + job_company_name: 'Acme', + job_company_website: 'acme.com', + }, + }, + enrichedAt: ENRICHED_AT, + companyIdByMatchKeyCache: new Map(), + overrideExistingValues: false, + }); + + expect(data.companyId).toBe('co-new'); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-person-name-param.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-person-name-param.spec.ts new file mode 100644 index 0000000000000..4b8cf687bf59d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-person-name-param.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { buildPersonNameParam } from 'src/logic-functions/utils/build-person-name-param'; + +describe('buildPersonNameParam', () => { + it('joins a full first and last name', () => { + expect(buildPersonNameParam({ firstName: 'Jane', lastName: 'Doe' })).toBe( + 'Jane Doe', + ); + }); + + it('returns undefined unless both name parts are present', () => { + expect(buildPersonNameParam({ firstName: 'Jane', lastName: '' })).toBeUndefined(); + expect(buildPersonNameParam({ firstName: '', lastName: 'Doe' })).toBeUndefined(); + expect(buildPersonNameParam({ firstName: '', lastName: null })).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-phones.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-phones.spec.ts new file mode 100644 index 0000000000000..915478b5aa0a5 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-phones.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { buildPhones } from 'src/logic-functions/utils/build-phones'; + +describe('buildPhones', () => { + it('uses the first present number as the primary phone', () => { + expect(buildPhones([null, '', '+15551234'])).toEqual({ + primaryPhoneNumber: '+15551234', + primaryPhoneCountryCode: '', + primaryPhoneCallingCode: '', + additionalPhones: null, + }); + }); + + it('keeps additional numbers and dedupes', () => { + expect( + buildPhones(['+15551234', '+15555678', '+15551234', '+15559999']), + ).toEqual({ + primaryPhoneNumber: '+15551234', + primaryPhoneCountryCode: '', + primaryPhoneCallingCode: '', + additionalPhones: [ + { number: '+15555678', countryCode: '', callingCode: '' }, + { number: '+15559999', countryCode: '', callingCode: '' }, + ], + }); + }); + + it('returns undefined when there is no number', () => { + expect(buildPhones([null, undefined])).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-skipped-result.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-skipped-result.spec.ts new file mode 100644 index 0000000000000..e491aa6ff130f --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-skipped-result.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { buildSkippedResult } from 'src/logic-functions/utils/build-skipped-result'; + +describe('buildSkippedResult', () => { + it('builds a successful skipped result with no updated fields', () => { + expect( + buildSkippedResult({ recordId: 'record-1', message: 'no identifier' }), + ).toEqual({ + success: true, + recordId: 'record-1', + status: 'SKIPPED', + updatedFields: [], + message: 'no identifier', + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-tool-record-ids.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-tool-record-ids.spec.ts new file mode 100644 index 0000000000000..fc905dd38f66d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/build-tool-record-ids.spec.ts @@ -0,0 +1,23 @@ +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([]); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/capitalize-name.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/capitalize-name.spec.ts new file mode 100644 index 0000000000000..8d02b57ea30e8 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/capitalize-name.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { capitalizeName } from 'src/logic-functions/utils/capitalize-name'; + +describe('capitalizeName', () => { + it('capitalizes a lowercase single token', () => { + expect(capitalizeName('sean')).toBe('Sean'); + }); + + it('capitalizes every whitespace-separated token', () => { + expect(capitalizeName('sean thorne')).toBe('Sean Thorne'); + }); + + it('capitalizes after hyphens and apostrophes', () => { + expect(capitalizeName('mary-jane')).toBe('Mary-Jane'); + expect(capitalizeName("o'brien")).toBe("O'Brien"); + }); + + it('leaves already-capitalized names untouched', () => { + expect(capitalizeName('Jane Doe')).toBe('Jane Doe'); + }); + + it('returns an empty string unchanged', () => { + expect(capitalizeName('')).toBe(''); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/chunk.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/chunk.spec.ts new file mode 100644 index 0000000000000..c5103786947bb --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/chunk.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { chunk } from 'src/logic-functions/utils/chunk'; + +describe('chunk', () => { + it('splits an array into chunks of the given size', () => { + expect(chunk({ items: [1, 2, 3, 4, 5], size: 2 })).toEqual([ + [1, 2], + [3, 4], + [5], + ]); + }); + + it('returns a single chunk when the array is smaller than the size', () => { + expect(chunk({ items: [1, 2], size: 10 })).toEqual([[1, 2]]); + }); + + it('returns an empty array for empty input', () => { + expect(chunk({ items: [], size: 10 })).toEqual([]); + }); + + it('returns the whole array as one chunk for a non-positive size', () => { + expect(chunk({ items: [1, 2, 3], size: 0 })).toEqual([[1, 2, 3]]); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/enrich-companies.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/enrich-companies.spec.ts new file mode 100644 index 0000000000000..75d837ed66475 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/enrich-companies.spec.ts @@ -0,0 +1,26 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { enrichCompanies } from 'src/logic-functions/utils/enrich-companies'; +import { postPdlBulkEnrich } from 'src/logic-functions/utils/post-pdl-enrich'; + +vi.mock('src/logic-functions/utils/post-pdl-enrich', () => ({ + postPdlBulkEnrich: vi.fn(() => Promise.resolve([])), +})); + +const postPdlBulkEnrichMock = vi.mocked(postPdlBulkEnrich); + +describe('enrichCompanies', () => { + beforeEach(() => { + postPdlBulkEnrichMock.mockClear(); + }); + + it('posts every record to the company bulk endpoint with only the provided params', async () => { + await enrichCompanies([{ website: 'acme.com' }, { pdlId: 'abc' }]); + + expect(postPdlBulkEnrichMock).toHaveBeenCalledTimes(1); + expect(postPdlBulkEnrichMock).toHaveBeenCalledWith({ + path: '/company/enrich/bulk', + requests: [{ website: 'acme.com' }, { pdl_id: 'abc' }], + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/enrich-people.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/enrich-people.spec.ts new file mode 100644 index 0000000000000..f3335650f56d8 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/enrich-people.spec.ts @@ -0,0 +1,32 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { enrichPeople } from 'src/logic-functions/utils/enrich-people'; +import { postPdlBulkEnrich } from 'src/logic-functions/utils/post-pdl-enrich'; + +vi.mock('src/logic-functions/utils/post-pdl-enrich', () => ({ + postPdlBulkEnrich: vi.fn(() => Promise.resolve([])), +})); + +const postPdlBulkEnrichMock = vi.mocked(postPdlBulkEnrich); + +describe('enrichPeople', () => { + beforeEach(() => { + postPdlBulkEnrichMock.mockClear(); + }); + + it('posts every record to the person bulk endpoint with only the provided params', async () => { + await enrichPeople([ + { email: 'jane@acme.com', minLikelihood: 6 }, + { pdlId: 'abc' }, + ]); + + expect(postPdlBulkEnrichMock).toHaveBeenCalledTimes(1); + expect(postPdlBulkEnrichMock).toHaveBeenCalledWith({ + path: '/person/bulk', + requests: [ + { email: 'jane@acme.com', min_likelihood: 6 }, + { pdl_id: 'abc' }, + ], + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-company-match-params.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-company-match-params.spec.ts new file mode 100644 index 0000000000000..021a5aeb1d58e --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-company-match-params.spec.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest'; + +import { COMPANY_NODE_MOCK } from 'src/logic-functions/__mocks__/company-node.mock'; +import { extractCompanyMatchParams } from 'src/logic-functions/utils/extract-company-match-params'; + +const INPUT = { records: [] }; + +describe('extractCompanyMatchParams', () => { + it('prefers an existing pdlId with the strong-identifier likelihood', () => { + expect( + extractCompanyMatchParams({ + node: { ...COMPANY_NODE_MOCK, pdlId: 'pdl-c' }, + input: INPUT, + }), + ).toEqual({ pdlId: 'pdl-c', minLikelihood: 2 }); + }); + + it('uses the domain and name with the strong-identifier likelihood', () => { + expect( + extractCompanyMatchParams({ + node: { ...COMPANY_NODE_MOCK, name: 'Acme' }, + input: INPUT, + }), + ).toEqual({ website: 'acme.com', name: 'Acme', minLikelihood: 2 }); + }); + + it('uses the linkedin profile as a strong identifier, stripped to the PDL path format', () => { + expect( + extractCompanyMatchParams({ + node: { + ...COMPANY_NODE_MOCK, + domainName: null, + linkedinLink: { + primaryLinkUrl: 'https://www.linkedin.com/company/acme/', + }, + name: 'Acme', + }, + input: INPUT, + }), + ).toEqual({ + profile: 'linkedin.com/company/acme', + name: 'Acme', + minLikelihood: 2, + }); + }); + + it('strips the scheme and www from the website before matching', () => { + expect( + extractCompanyMatchParams({ + node: { + ...COMPANY_NODE_MOCK, + domainName: { primaryLinkUrl: 'https://www.acme.com/' }, + name: 'Acme', + }, + input: INPUT, + }), + ).toEqual({ website: 'acme.com', name: 'Acme', minLikelihood: 2 }); + }); + + it('raises the likelihood when only a name is available', () => { + expect( + extractCompanyMatchParams({ + node: { ...COMPANY_NODE_MOCK, domainName: null, name: 'Acme' }, + input: INPUT, + }), + ).toEqual({ name: 'Acme', minLikelihood: 6 }); + }); + + it('honors an explicit minLikelihood from the input', () => { + expect( + extractCompanyMatchParams({ + node: { ...COMPANY_NODE_MOCK, name: 'Acme' }, + input: { records: [], minLikelihood: 9 }, + }), + ).toMatchObject({ minLikelihood: 9 }); + }); + + it('returns undefined when there is no usable identifier', () => { + expect( + extractCompanyMatchParams({ + node: { ...COMPANY_NODE_MOCK, domainName: null, name: '' }, + input: INPUT, + }), + ).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-pdl-error-message.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-pdl-error-message.spec.ts new file mode 100644 index 0000000000000..d9577c07762ca --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-pdl-error-message.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { extractPdlErrorMessage } from 'src/logic-functions/utils/extract-pdl-error-message'; + +describe('extractPdlErrorMessage', () => { + it('reads the nested PDL error message', () => { + expect( + extractPdlErrorMessage({ json: { error: { message: 'boom' } }, httpStatus: 500 }), + ).toBe('boom'); + }); + + it('falls back to a generic message when none is present', () => { + expect(extractPdlErrorMessage({ json: {}, httpStatus: 503 })).toBe( + 'PDL request failed (HTTP 503).', + ); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-person-match-params.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-person-match-params.spec.ts new file mode 100644 index 0000000000000..371ff1e3d8b5d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-person-match-params.spec.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; + +import { PERSON_NODE_MOCK } from 'src/logic-functions/__mocks__/person-node.mock'; +import { extractPersonMatchParams } from 'src/logic-functions/utils/extract-person-match-params'; + +describe('extractPersonMatchParams', () => { + it('prefers an existing pdlId and uses the strong-identifier likelihood', () => { + expect( + extractPersonMatchParams({ + node: { ...PERSON_NODE_MOCK, pdlId: 'pdl-1' }, + input: { records: [] }, + }), + ).toEqual({ pdlId: 'pdl-1', minLikelihood: 2 }); + }); + + it('uses the linkedin profile with the strong-identifier likelihood', () => { + expect( + extractPersonMatchParams({ + node: PERSON_NODE_MOCK, + input: { records: [] }, + }), + ).toEqual({ + profile: 'https://linkedin.com/in/existing', + minLikelihood: 2, + }); + }); + + it('pairs a name with the person company and uses the weak-identifier likelihood', () => { + expect( + extractPersonMatchParams({ + node: { + ...PERSON_NODE_MOCK, + linkedinLink: null, + name: { firstName: 'Jane', lastName: 'Doe' }, + company: { id: 'co-1', name: 'Acme' }, + }, + input: { records: [] }, + }), + ).toEqual({ name: 'Jane Doe', company: 'Acme', minLikelihood: 6 }); + }); + + it('returns undefined for a name with no anchoring identifier or company', () => { + expect( + extractPersonMatchParams({ + node: { + ...PERSON_NODE_MOCK, + linkedinLink: null, + name: { firstName: 'Jane', lastName: 'Doe' }, + }, + input: { records: [] }, + }), + ).toBeUndefined(); + }); + + it('honors an explicit minLikelihood from the input', () => { + expect( + extractPersonMatchParams({ + node: PERSON_NODE_MOCK, + input: { + records: [], + minLikelihood: 9, + }, + }), + ).toMatchObject({ minLikelihood: 9 }); + }); + + it('returns undefined when there is no usable identifier', () => { + expect( + extractPersonMatchParams({ + node: { ...PERSON_NODE_MOCK, linkedinLink: null }, + input: { records: [] }, + }), + ).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-record-ids.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-record-ids.spec.ts new file mode 100644 index 0000000000000..bac97a549edcb --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/extract-record-ids.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { extractRecordIds } from 'src/logic-functions/utils/extract-record-ids'; + +describe('extractRecordIds', () => { + it('extracts ids from record objects', () => { + expect(extractRecordIds([{ id: 'a' }, { id: 'b' }])).toEqual(['a', 'b']); + }); + + it('accepts plain id strings', () => { + expect(extractRecordIds(['a', 'b'])).toEqual(['a', 'b']); + }); + + it('drops entries without a usable id', () => { + expect( + extractRecordIds([{ id: 'a' }, { id: null }, {}, { id: '' }, 'c']), + ).toEqual(['a', 'c']); + }); + + it('accepts a single record object (not wrapped in an array)', () => { + expect(extractRecordIds({ id: 'a' })).toEqual(['a']); + }); + + it('accepts a single id string (not wrapped in an array)', () => { + expect(extractRecordIds('a')).toEqual(['a']); + }); + + it('returns an empty array for nullish input', () => { + expect(extractRecordIds(undefined as unknown as [])).toEqual([]); + expect(extractRecordIds(null as unknown as [])).toEqual([]); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-company-id-by-filter.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-company-id-by-filter.spec.ts new file mode 100644 index 0000000000000..300ed487ced7e --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-company-id-by-filter.spec.ts @@ -0,0 +1,42 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; +import { describe, expect, it, vi } from 'vitest'; + +import { findCompanyIdByFilter } from 'src/logic-functions/utils/find-company-id-by-filter'; + +describe('findCompanyIdByFilter', () => { + it('returns the first matching company id and forwards the filter', async () => { + const query = vi.fn(() => + Promise.resolve({ companies: { edges: [{ node: { id: 'co-1' } }] } }), + ); + const client = { query } as unknown as CoreApiClient; + const filter = { pdlId: { eq: 'pdl-co-1' } }; + + const result = await findCompanyIdByFilter({ client, filter }); + + expect(result).toBe('co-1'); + expect(query).toHaveBeenCalledWith({ + companies: { + __args: { filter, first: 1 }, + edges: { node: { id: true } }, + }, + }); + }); + + it('returns undefined when there are no matches', async () => { + const query = vi.fn(() => Promise.resolve({ companies: { edges: [] } })); + const client = { query } as unknown as CoreApiClient; + + expect( + await findCompanyIdByFilter({ client, filter: { name: { eq: 'Acme' } } }), + ).toBeUndefined(); + }); + + it('returns undefined when the companies connection is absent', async () => { + const query = vi.fn(() => Promise.resolve({})); + const client = { query } as unknown as CoreApiClient; + + expect( + await findCompanyIdByFilter({ client, filter: { name: { eq: 'Acme' } } }), + ).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-company-id.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-company-id.spec.ts new file mode 100644 index 0000000000000..442fd52021c50 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-company-id.spec.ts @@ -0,0 +1,118 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; +import { describe, expect, it, vi } from 'vitest'; + +import { findCompanyId } from 'src/logic-functions/utils/find-company-id'; + +type CompanyQueryRequest = { + companies: { __args: { filter: Record } }; +}; + +const filterKeyOf = (request: unknown) => + Object.keys((request as CompanyQueryRequest).companies.__args.filter)[0]; + +const ALL_KEYS = { + pdlId: 'pdl-co-1', + website: 'acme.com', + linkedinUrl: 'https://linkedin.com/company/acme', + name: 'Acme', +}; + +describe('findCompanyId', () => { + it('matches by pdlId first and does not query the lower-priority keys', async () => { + const query = vi.fn((request: unknown) => + Promise.resolve( + filterKeyOf(request) === 'pdlId' + ? { companies: { edges: [{ node: { id: 'co-pdl' } }] } } + : { companies: { edges: [] } }, + ), + ); + const client = { query } as unknown as CoreApiClient; + + const result = await findCompanyId({ client, matchKeys: ALL_KEYS }); + + expect(result).toBe('co-pdl'); + expect(query).toHaveBeenCalledTimes(1); + }); + + it('falls through to lower-priority keys in order until one matches', async () => { + const queriedKeys: string[] = []; + const query = vi.fn((request: unknown) => { + const key = filterKeyOf(request); + queriedKeys.push(key); + + return Promise.resolve( + key === 'name' + ? { companies: { edges: [{ node: { id: 'co-name' } }] } } + : { companies: { edges: [] } }, + ); + }); + const client = { query } as unknown as CoreApiClient; + + const result = await findCompanyId({ client, matchKeys: ALL_KEYS }); + + expect(result).toBe('co-name'); + expect(queriedKeys).toEqual([ + 'pdlId', + 'domainName', + 'linkedinLink', + 'name', + ]); + }); + + it('skips filters whose match key is absent', async () => { + const queriedKeys: string[] = []; + const query = vi.fn((request: unknown) => { + queriedKeys.push(filterKeyOf(request)); + + return Promise.resolve({ companies: { edges: [] } }); + }); + const client = { query } as unknown as CoreApiClient; + + await findCompanyId({ client, matchKeys: { name: 'Acme' } }); + + expect(queriedKeys).toEqual(['name']); + }); + + it('returns undefined when no key matches', async () => { + const query = vi.fn(() => Promise.resolve({ companies: { edges: [] } })); + const client = { query } as unknown as CoreApiClient; + + expect( + await findCompanyId({ client, matchKeys: ALL_KEYS }), + ).toBeUndefined(); + }); + + it('returns undefined and issues no query when no keys are provided', async () => { + const query = vi.fn(); + const client = { query } as unknown as CoreApiClient; + + expect(await findCompanyId({ client, matchKeys: {} })).toBeUndefined(); + expect(query).not.toHaveBeenCalled(); + }); + + it('does not link a name match when it is ambiguous', async () => { + const query = vi.fn(() => + Promise.resolve({ + companies: { + edges: [{ node: { id: 'co-1' } }, { node: { id: 'co-2' } }], + }, + }), + ); + const client = { query } as unknown as CoreApiClient; + + expect( + await findCompanyId({ client, matchKeys: { name: 'Acme' } }), + ).toBeUndefined(); + }); + + it('links a unique name match', async () => { + const query = vi.fn(() => + Promise.resolve({ companies: { edges: [{ node: { id: 'co-1' } }] } }), + ); + const client = { query } as unknown as CoreApiClient; + + expect(await findCompanyId({ client, matchKeys: { name: 'Acme' } })).toBe( + 'co-1', + ); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-existing-workflow-id.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-existing-workflow-id.spec.ts new file mode 100644 index 0000000000000..0aa9b26870194 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-existing-workflow-id.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock'; +import { findExistingWorkflowId } from 'src/logic-functions/utils/find-existing-workflow-id'; + +describe('findExistingWorkflowId', () => { + it('returns the id of the first matching workflow', async () => { + const client = createCoreApiClientMock({ + queryResult: { workflows: { edges: [{ node: { id: 'workflow-1' } }] } }, + }); + + await expect( + findExistingWorkflowId({ client, name: 'Enrich company' }), + ).resolves.toBe('workflow-1'); + }); + + it('returns undefined when no workflow matches', async () => { + const client = createCoreApiClientMock({ + queryResult: { workflows: { edges: [] } }, + }); + + await expect( + findExistingWorkflowId({ client, name: 'Enrich company' }), + ).resolves.toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-or-create-current-company.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-or-create-current-company.spec.ts new file mode 100644 index 0000000000000..d1f04e04272c4 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/find-or-create-current-company.spec.ts @@ -0,0 +1,203 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; +import { describe, expect, it, vi } from 'vitest'; + +import { findOrCreateCurrentCompany } from 'src/logic-functions/utils/find-or-create-current-company'; +import { type PdlPersonData } from 'src/types/pdl-person-data'; + +type CompanyQueryRequest = { + companies: { __args: { filter: Record } }; +}; +type CreateCompanyRequest = { + createCompany: { __args: { data: Record } }; +}; + +const filterOf = (request: unknown) => + (request as CompanyQueryRequest).companies.__args.filter; +const createDataOf = (request: unknown) => + (request as CreateCompanyRequest).createCompany.__args.data; + +const newCache = () => new Map(); + +describe('findOrCreateCurrentCompany', () => { + it('returns undefined and issues no calls when PDL gives no identifiers', async () => { + const query = vi.fn(); + const mutation = vi.fn(); + const client = { query, mutation } as unknown as CoreApiClient; + + const result = await findOrCreateCurrentCompany({ + client, + personData: {} as PdlPersonData, + companyIdByMatchKeyCache: newCache(), + }); + + expect(result).toBeUndefined(); + expect(query).not.toHaveBeenCalled(); + expect(mutation).not.toHaveBeenCalled(); + }); + + it('links an existing company matched by pdlId alone', async () => { + const query = vi.fn(() => + Promise.resolve({ companies: { edges: [{ node: { id: 'co-existing' } }] } }), + ); + const mutation = vi.fn(); + const client = { query, mutation } as unknown as CoreApiClient; + + const result = await findOrCreateCurrentCompany({ + client, + personData: { job_company_id: 'pdl-co-1' } as PdlPersonData, + companyIdByMatchKeyCache: newCache(), + }); + + expect(result).toBe('co-existing'); + expect(mutation).not.toHaveBeenCalled(); + }); + + it('attempts a pdlId match but does not create without a name or website', async () => { + const query = vi.fn(() => Promise.resolve({ companies: { edges: [] } })); + const mutation = vi.fn(); + const client = { query, mutation } as unknown as CoreApiClient; + + const result = await findOrCreateCurrentCompany({ + client, + personData: { job_company_id: 'pdl-co-1' } as PdlPersonData, + companyIdByMatchKeyCache: newCache(), + }); + + expect(result).toBeUndefined(); + expect(query).toHaveBeenCalled(); + expect(mutation).not.toHaveBeenCalled(); + }); + + it('returns an existing company id without creating one', async () => { + const query = vi.fn(() => + Promise.resolve({ companies: { edges: [{ node: { id: 'co-existing' } }] } }), + ); + const mutation = vi.fn(); + const client = { query, mutation } as unknown as CoreApiClient; + + const result = await findOrCreateCurrentCompany({ + client, + personData: { + job_company_name: 'Acme', + } as PdlPersonData, + companyIdByMatchKeyCache: newCache(), + }); + + expect(result).toBe('co-existing'); + expect(mutation).not.toHaveBeenCalled(); + }); + + it('creates a company from the PDL data when none matches', async () => { + const query = vi.fn(() => Promise.resolve({ companies: { edges: [] } })); + let createdData: Record | undefined; + const mutation = vi.fn((request: unknown) => { + createdData = createDataOf(request); + + return Promise.resolve({ createCompany: { id: 'co-new' } }); + }); + const client = { query, mutation } as unknown as CoreApiClient; + + const result = await findOrCreateCurrentCompany({ + client, + personData: { + job_company_id: 'pdl-co-2', + job_company_name: 'Acme', + job_company_website: 'acme.com', + } as PdlPersonData, + companyIdByMatchKeyCache: newCache(), + }); + + expect(result).toBe('co-new'); + expect(createdData).toMatchObject({ name: 'Acme', pdlId: 'pdl-co-2' }); + }); + + it('throws when the create mutation returns no company id', async () => { + const query = vi.fn(() => Promise.resolve({ companies: { edges: [] } })); + const mutation = vi.fn(() => Promise.resolve({ createCompany: null })); + const client = { query, mutation } as unknown as CoreApiClient; + + await expect( + findOrCreateCurrentCompany({ + client, + personData: { + job_company_name: 'Acme', + } as PdlPersonData, + companyIdByMatchKeyCache: newCache(), + }), + ).rejects.toThrow('Failed to create company: no id returned.'); + }); + + it('re-finds the winner when a concurrent create hits a unique violation', async () => { + let created = false; + const query = vi.fn((request: unknown) => + Promise.resolve( + created && 'pdlId' in filterOf(request) + ? { companies: { edges: [{ node: { id: 'co-race' } }] } } + : { companies: { edges: [] } }, + ), + ); + const mutation = vi.fn(() => { + created = true; + + return Promise.reject( + new Error('duplicate key value violates unique constraint'), + ); + }); + const client = { query, mutation } as unknown as CoreApiClient; + + const result = await findOrCreateCurrentCompany({ + client, + personData: { + job_company_id: 'pdl-co-3', + job_company_name: 'Acme', + } as PdlPersonData, + companyIdByMatchKeyCache: newCache(), + }); + + expect(result).toBe('co-race'); + }); + + it('rethrows a non-unique-violation create error', async () => { + const query = vi.fn(() => Promise.resolve({ companies: { edges: [] } })); + const mutation = vi.fn(() => Promise.reject(new Error('network down'))); + const client = { query, mutation } as unknown as CoreApiClient; + + await expect( + findOrCreateCurrentCompany({ + client, + personData: { + job_company_name: 'Acme', + } as PdlPersonData, + companyIdByMatchKeyCache: newCache(), + }), + ).rejects.toThrow('network down'); + }); + + it('reuses a cached company id for an identical company without re-querying', async () => { + const query = vi.fn(() => + Promise.resolve({ companies: { edges: [{ node: { id: 'co-existing' } }] } }), + ); + const mutation = vi.fn(); + const client = { query, mutation } as unknown as CoreApiClient; + const companyIdByMatchKeyCache = newCache(); + const personData = { + job_company_name: 'Acme', + job_company_website: 'acme.com', + } as PdlPersonData; + + const first = await findOrCreateCurrentCompany({ + client, + personData, + companyIdByMatchKeyCache, + }); + const second = await findOrCreateCurrentCompany({ + client, + personData, + companyIdByMatchKeyCache, + }); + + expect(first).toBe('co-existing'); + expect(second).toBe('co-existing'); + expect(query).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/get-pdl-api-key.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/get-pdl-api-key.spec.ts new file mode 100644 index 0000000000000..5ec1542792360 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/get-pdl-api-key.spec.ts @@ -0,0 +1,38 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { getPdlApiKey } from 'src/logic-functions/utils/get-pdl-api-key'; +import { PdlConfigError } from 'src/logic-functions/errors/pdl-config-error'; + +describe('getPdlApiKey', () => { + let previousApiKey: string | undefined; + + beforeEach(() => { + previousApiKey = process.env.PDL_API_KEY; + }); + + afterEach(() => { + if (previousApiKey === undefined) { + delete process.env.PDL_API_KEY; + } else { + process.env.PDL_API_KEY = previousApiKey; + } + }); + + it('returns the configured key', () => { + process.env.PDL_API_KEY = 'secret-key'; + + expect(getPdlApiKey()).toBe('secret-key'); + }); + + it('throws a PdlConfigError when the key is missing', () => { + delete process.env.PDL_API_KEY; + + expect(() => getPdlApiKey()).toThrow(PdlConfigError); + }); + + it('throws a PdlConfigError when the key is blank', () => { + process.env.PDL_API_KEY = ' '; + + expect(() => getPdlApiKey()).toThrow(PdlConfigError); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-address.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-address.spec.ts new file mode 100644 index 0000000000000..d8fc27950d713 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-address.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; + +import { isEmptyAddress } from 'src/logic-functions/utils/is-empty-address'; + +describe('isEmptyAddress', () => { + it('is true when every address text field is empty or missing', () => { + expect(isEmptyAddress(null)).toBe(true); + expect( + isEmptyAddress({ + addressStreet1: '', + addressCity: ' ', + addressCountry: '', + }), + ).toBe(true); + }); + + it('is false when at least one address field is present', () => { + expect(isEmptyAddress({ addressCity: 'San Francisco' })).toBe(false); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-emails.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-emails.spec.ts new file mode 100644 index 0000000000000..359db057fa2f4 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-emails.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; + +import { isEmptyEmails } from 'src/logic-functions/utils/is-empty-emails'; + +describe('isEmptyEmails', () => { + it('is true when there is no primary email', () => { + expect(isEmptyEmails(null)).toBe(true); + expect(isEmptyEmails({ primaryEmail: '' })).toBe(true); + }); + + it('is false when a primary email is present', () => { + expect(isEmptyEmails({ primaryEmail: 'jane@acme.com' })).toBe(false); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-full-name.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-full-name.spec.ts new file mode 100644 index 0000000000000..bbabcb37fe556 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-full-name.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; + +import { isEmptyFullName } from 'src/logic-functions/utils/is-empty-full-name'; + +describe('isEmptyFullName', () => { + it('is true when both first and last name are empty or missing', () => { + expect(isEmptyFullName(null)).toBe(true); + expect(isEmptyFullName({ firstName: '', lastName: '' })).toBe(true); + }); + + it('is false when at least one name part is present', () => { + expect(isEmptyFullName({ firstName: 'Jane', lastName: '' })).toBe(false); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-links.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-links.spec.ts new file mode 100644 index 0000000000000..8b393b11e5361 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-links.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; + +import { isEmptyLinks } from 'src/logic-functions/utils/is-empty-links'; + +describe('isEmptyLinks', () => { + it('is true when there is no primary link url', () => { + expect(isEmptyLinks(null)).toBe(true); + expect(isEmptyLinks({ primaryLinkUrl: '' })).toBe(true); + }); + + it('is false when a primary link url is present', () => { + expect(isEmptyLinks({ primaryLinkUrl: 'https://acme.com' })).toBe(false); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-phones.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-phones.spec.ts new file mode 100644 index 0000000000000..aff80a9eab2d0 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-phones.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; + +import { isEmptyPhones } from 'src/logic-functions/utils/is-empty-phones'; + +describe('isEmptyPhones', () => { + it('is true when there is no primary phone number', () => { + expect(isEmptyPhones(null)).toBe(true); + expect(isEmptyPhones({ primaryPhoneNumber: '' })).toBe(true); + }); + + it('is false when a primary phone number is present', () => { + expect(isEmptyPhones({ primaryPhoneNumber: '5551234' })).toBe(false); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-text.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-text.spec.ts new file mode 100644 index 0000000000000..dca6a40ff40eb --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/is-empty-text.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; + +import { isEmptyText } from 'src/logic-functions/utils/is-empty-text'; + +describe('isEmptyText', () => { + it('is true for empty, whitespace and non-string values', () => { + expect(isEmptyText('')).toBe(true); + expect(isEmptyText(' ')).toBe(true); + expect(isEmptyText(null)).toBe(true); + }); + + it('is false for a non-empty string', () => { + expect(isEmptyText('hello')).toBe(false); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/map-company.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/map-company.spec.ts new file mode 100644 index 0000000000000..185aa785cdd35 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/map-company.spec.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; + +import { PDL_COMPANY_DATA_MOCK } from 'src/logic-functions/__mocks__/pdl-company-data.mock'; +import { mapCompany } from 'src/logic-functions/utils/map-company'; + +describe('mapCompany', () => { + it('maps standard fields including the address composite', () => { + const { standard } = mapCompany(PDL_COMPANY_DATA_MOCK); + + expect(standard.name).toBe('Acme Corp'); + expect(standard.domainName).toMatchObject({ primaryLinkUrl: 'acme.com' }); + expect(standard.address).toMatchObject({ + addressCity: 'San Francisco', + addressPostcode: '94107', + addressCountry: 'United States', + }); + }); + + it('converts USD funding to currency micros', () => { + const { pdl } = mapCompany(PDL_COMPANY_DATA_MOCK); + + expect(pdl.pdlTotalFunding).toEqual({ + amountMicros: 1_234_500_000, + currencyCode: 'USD', + }); + }); + + it('maps select and multi-select funding fields, dropping unknowns', () => { + const { pdl } = mapCompany(PDL_COMPANY_DATA_MOCK); + + expect(pdl.pdlSizeRange).toBe('FIFTY_ONE_TO_TWO_HUNDRED'); + expect(pdl.pdlCompanyType).toBe('PRIVATE'); + expect(pdl.pdlIndustry).toBe('ACCOUNTING'); + expect(pdl.pdlLocationContinent).toBe('NORTH_AMERICA'); + expect(pdl.pdlLatestFundingStage).toBe('SERIES_A'); + expect(pdl.pdlFundingStages).toEqual(['SEED', 'SERIES_A']); + }); + + it('maps numbers, dates, arrays and raw json', () => { + const { pdl } = mapCompany(PDL_COMPANY_DATA_MOCK); + + expect(pdl.pdlEmployeeCount).toBe(120); + expect(pdl.pdlFoundedYear).toBe(2001); + expect(pdl.pdlLastFundingDate).toBe('2020-05-01'); + expect(pdl.pdlTags).toEqual(['saas', 'b2b']); + expect(pdl.pdlNaics).toEqual([{ naics_code: '541511' }]); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/map-person.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/map-person.spec.ts new file mode 100644 index 0000000000000..8782629803cfa --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/map-person.spec.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; + +import { PDL_PERSON_DATA_MOCK } from 'src/logic-functions/__mocks__/pdl-person-data.mock'; +import { mapPerson } from 'src/logic-functions/utils/map-person'; + +describe('mapPerson', () => { + it('maps standard fields', () => { + const { standard } = mapPerson(PDL_PERSON_DATA_MOCK); + + expect(standard.name).toEqual({ firstName: 'Jane', lastName: 'Doe' }); + expect(standard.emails).toEqual({ + primaryEmail: 'jane.doe@acme.com', + additionalEmails: ['jane@personal.com'], + }); + expect(standard.jobTitle).toBe('Chief Executive Officer'); + expect(standard.linkedinLink).toMatchObject({ + primaryLinkUrl: 'https://linkedin.com/in/janedoe', + }); + }); + + it('maps pdl scalar, select and number fields', () => { + const { pdl } = mapPerson(PDL_PERSON_DATA_MOCK); + + expect(pdl.pdlId).toBe('pdl-person-1'); + expect(pdl.pdlJobRole).toBe('ENGINEERING'); + expect(pdl.pdlIndustry).toBe('ACCOUNTING'); + expect(pdl.pdlInferredSalary).toBe('FROM_45000_TO_55000'); + }); + + it('does not map company attributes onto the person', () => { + const { pdl } = mapPerson(PDL_PERSON_DATA_MOCK); + + expect('pdlJobCompanyName' in pdl).toBe(false); + expect('pdlJobCompanySize' in pdl).toBe(false); + }); + + it('drops unknown multi-select values', () => { + const { pdl } = mapPerson(PDL_PERSON_DATA_MOCK); + + expect(pdl.pdlSeniority).toEqual(['CXO']); + }); + + it('dedupes array fields and passes raw json through', () => { + const { pdl } = mapPerson(PDL_PERSON_DATA_MOCK); + + expect(pdl.pdlSkills).toEqual(['leadership', 'strategy']); + expect(pdl.pdlExperience).toEqual([{ company: { name: 'Acme' } }]); + }); + + it('expands partial dates and builds the address with addressPostcode', () => { + const { pdl } = mapPerson(PDL_PERSON_DATA_MOCK); + + expect(pdl.pdlBirthDate).toBe('1990-01-01'); + expect(pdl.pdlLocation).toMatchObject({ + addressCity: 'San Francisco', + addressState: 'California', + addressPostcode: '94107', + addressCountry: 'United States', + }); + }); + + it('omits fields PDL did not return', () => { + const { pdl } = mapPerson(PDL_PERSON_DATA_MOCK); + + expect('pdlSex' in pdl).toBe(false); + }); + + it('tolerates PDL returning presence-flag booleans for email and phone fields', () => { + const { standard } = mapPerson({ + ...PDL_PERSON_DATA_MOCK, + emails: true as never, + personal_emails: true as never, + phone_numbers: true as never, + }); + + expect(standard.emails).toEqual({ + primaryEmail: 'jane.doe@acme.com', + additionalEmails: null, + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/normalize-domain.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/normalize-domain.spec.ts new file mode 100644 index 0000000000000..68caf0ba2413b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/normalize-domain.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; + +import { normalizeDomain } from 'src/logic-functions/utils/normalize-domain'; + +describe('normalizeDomain', () => { + it('reduces variants to the same bare host', () => { + expect(normalizeDomain('acme.com')).toBe('acme.com'); + expect(normalizeDomain('https://www.Acme.com/')).toBe('acme.com'); + expect(normalizeDomain('HTTP://acme.com/careers')).toBe('acme.com'); + expect(normalizeDomain('www.acme.com')).toBe('acme.com'); + }); + + it('returns undefined for blank or missing values', () => { + expect(normalizeDomain('')).toBeUndefined(); + expect(normalizeDomain(' ')).toBeUndefined(); + expect(normalizeDomain(null)).toBeUndefined(); + expect(normalizeDomain(undefined)).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/normalize-linkedin-url.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/normalize-linkedin-url.spec.ts new file mode 100644 index 0000000000000..d398c4192c4a5 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/normalize-linkedin-url.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; + +import { normalizeLinkedinUrl } from 'src/logic-functions/utils/normalize-linkedin-url'; + +describe('normalizeLinkedinUrl', () => { + it('canonicalizes scheme/www/trailing slash while keeping the path', () => { + expect(normalizeLinkedinUrl('linkedin.com/company/acme')).toBe( + 'linkedin.com/company/acme', + ); + expect(normalizeLinkedinUrl('https://www.linkedin.com/company/acme/')).toBe( + 'linkedin.com/company/acme', + ); + expect(normalizeLinkedinUrl('HTTPS://LinkedIn.com/company/Acme')).toBe( + 'linkedin.com/company/acme', + ); + }); + + it('returns undefined for blank or missing values', () => { + expect(normalizeLinkedinUrl('')).toBeUndefined(); + expect(normalizeLinkedinUrl(null)).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/now-iso.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/now-iso.spec.ts new file mode 100644 index 0000000000000..3d275973ecb13 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/now-iso.spec.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; + +import { nowIso } from 'src/logic-functions/utils/now-iso'; + +describe('nowIso', () => { + it('returns an ISO-8601 timestamp string', () => { + expect(nowIso()).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/parse-geo.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/parse-geo.spec.ts new file mode 100644 index 0000000000000..8e1bb65767c95 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/parse-geo.spec.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; + +import { parseGeo } from 'src/logic-functions/utils/parse-geo'; + +describe('parseGeo', () => { + it('parses a "lat,lng" string', () => { + expect(parseGeo('37.77,-122.41')).toEqual({ lat: 37.77, lng: -122.41 }); + }); + + it('returns nulls for missing or malformed input', () => { + expect(parseGeo(undefined)).toEqual({ lat: null, lng: null }); + expect(parseGeo('not-a-geo')).toEqual({ lat: null, lng: null }); + }); + + it('rejects a single value, extra components, or out-of-range coordinates', () => { + expect(parseGeo('37.77')).toEqual({ lat: null, lng: null }); + expect(parseGeo('37.77,-122.41,5')).toEqual({ lat: null, lng: null }); + expect(parseGeo('200,0')).toEqual({ lat: null, lng: null }); + expect(parseGeo('0,200')).toEqual({ lat: null, lng: null }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/parse-partial-date.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/parse-partial-date.spec.ts new file mode 100644 index 0000000000000..eeae72429a054 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/parse-partial-date.spec.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { parsePartialDate } from 'src/logic-functions/utils/parse-partial-date'; + +describe('parsePartialDate', () => { + it('expands a year to a full date', () => { + expect(parsePartialDate('1990')).toBe('1990-01-01'); + }); + + it('expands a year-month to a full date', () => { + expect(parsePartialDate('2020-05')).toBe('2020-05-01'); + }); + + it('keeps a full date', () => { + expect(parsePartialDate('2020-05-17')).toBe('2020-05-17'); + }); + + it('returns undefined for an unparseable value', () => { + expect(parsePartialDate('not-a-date')).toBeUndefined(); + expect(parsePartialDate(undefined)).toBeUndefined(); + }); + + it('rejects out-of-range or non-existent calendar dates', () => { + expect(parsePartialDate('2020-13')).toBeUndefined(); + expect(parsePartialDate('2020-00-01')).toBeUndefined(); + expect(parsePartialDate('1990-02-31')).toBeUndefined(); + expect(parsePartialDate('2020-04-31')).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/parse-pdl-item.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/parse-pdl-item.spec.ts new file mode 100644 index 0000000000000..a742fbc63d38d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/parse-pdl-item.spec.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest'; + +import { parsePdlItem } from 'src/logic-functions/utils/parse-pdl-item'; + +describe('parsePdlItem', () => { + it('maps a 200 item to a matched outcome with data and likelihood', () => { + expect( + parsePdlItem({ item: { status: 200, likelihood: 8, data: { id: 'x' } } }), + ).toEqual({ + outcome: 'matched', + httpStatus: 200, + likelihood: 8, + data: { id: 'x' }, + }); + }); + + it('defaults a missing status to 200 and matches when data is present', () => { + expect(parsePdlItem({ item: { data: { id: 'x' } } })).toEqual({ + outcome: 'matched', + httpStatus: 200, + likelihood: undefined, + data: { id: 'x' }, + }); + }); + + it('matches a 200 item whose record fields are at the top level (company bulk shape)', () => { + expect( + parsePdlItem({ + item: { status: 200, likelihood: 6, id: 'x', name: 'Acme' }, + }), + ).toEqual({ + outcome: 'matched', + httpStatus: 200, + likelihood: 6, + data: { id: 'x', name: 'Acme' }, + }); + }); + + it('treats a 200 item carrying only the envelope as not_found', () => { + expect(parsePdlItem({ item: { status: 200, likelihood: 6 } })).toEqual({ + outcome: 'not_found', + httpStatus: 200, + }); + }); + + it('rejects a match whose likelihood is below the requested threshold', () => { + expect( + parsePdlItem({ + item: { status: 200, likelihood: 3, data: { id: 'x' } }, + requestedMinLikelihood: 6, + }), + ).toEqual({ outcome: 'not_found', httpStatus: 200 }); + }); + + it('keeps a match whose likelihood meets the requested threshold', () => { + expect( + parsePdlItem({ + item: { status: 200, likelihood: 6, data: { id: 'x' } }, + requestedMinLikelihood: 6, + }), + ).toMatchObject({ outcome: 'matched', likelihood: 6 }); + }); + + it('maps a 404 item to not_found', () => { + expect(parsePdlItem({ item: { status: 404 } })).toEqual({ + outcome: 'not_found', + httpStatus: 404, + }); + }); + + it('maps a non-2xx item to an error with the PDL message', () => { + expect( + parsePdlItem({ item: { status: 500, error: { message: 'boom' } } }), + ).toEqual({ + outcome: 'error', + httpStatus: 500, + message: 'boom', + }); + }); + + it('treats a non-object item as a malformed error', () => { + expect(parsePdlItem({ item: undefined })).toEqual({ + outcome: 'error', + httpStatus: 0, + message: 'People Data Labs returned a malformed response item.', + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/pick-multi-select.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/pick-multi-select.spec.ts new file mode 100644 index 0000000000000..41f8c7b805702 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/pick-multi-select.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; + +import { pickMultiSelect } from 'src/logic-functions/utils/pick-multi-select'; + +const LEVELS = new Set(['CXO', 'VP', 'DIRECTOR']); + +describe('pickMultiSelect', () => { + it('keeps allowed values and drops unknown ones', () => { + expect( + pickMultiSelect({ rawValues: ['cxo', 'vp', 'unknown'], allowedValues: LEVELS }), + ).toEqual(['CXO', 'VP']); + }); + + it('dedupes repeated values', () => { + expect( + pickMultiSelect({ rawValues: ['cxo', 'cxo'], allowedValues: LEVELS }), + ).toEqual(['CXO']); + }); + + it('returns undefined for a non-array or when nothing is valid', () => { + expect( + pickMultiSelect({ rawValues: 'cxo', allowedValues: LEVELS }), + ).toBeUndefined(); + expect( + pickMultiSelect({ rawValues: ['unknown'], allowedValues: LEVELS }), + ).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/pick-select.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/pick-select.spec.ts new file mode 100644 index 0000000000000..2d9e3fc00223f --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/pick-select.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; + +import { pickSelect } from 'src/logic-functions/utils/pick-select'; +import { sizeTransform } from 'src/logic-functions/utils/size-transform'; + +const ROLES = new Set(['ENGINEERING', 'SALES']); +const SIZES = new Set(['ONE_TO_TEN', 'ELEVEN_TO_FIFTY']); + +describe('pickSelect', () => { + it('normalizes and returns an allowed value', () => { + expect(pickSelect({ raw: 'engineering', allowedValues: ROLES })).toBe( + 'ENGINEERING', + ); + }); + + it('returns undefined for a value outside the option set', () => { + expect( + pickSelect({ raw: 'marketing', allowedValues: ROLES }), + ).toBeUndefined(); + }); + + it('returns undefined for empty or non-string input', () => { + expect(pickSelect({ raw: '', allowedValues: ROLES })).toBeUndefined(); + expect( + pickSelect({ raw: undefined, allowedValues: ROLES }), + ).toBeUndefined(); + }); + + it('applies a custom transform before checking the option set', () => { + expect( + pickSelect({ + raw: '1-10', + allowedValues: SIZES, + transform: sizeTransform, + }), + ).toBe('ONE_TO_TEN'); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/pick-writable-standard.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/pick-writable-standard.spec.ts new file mode 100644 index 0000000000000..10cd637c6e1cc --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/pick-writable-standard.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; + +import { pickWritableStandard } from 'src/logic-functions/utils/pick-writable-standard'; +import { isEmptyText } from 'src/logic-functions/utils/is-empty-text'; + +describe('pickWritableStandard', () => { + it('keeps a mapped field only when the current value is empty', () => { + const writable = pickWritableStandard({ + standard: { name: 'Acme', jobTitle: 'CEO' }, + current: { name: '', jobTitle: 'Existing' }, + emptyChecks: { name: isEmptyText, jobTitle: isEmptyText }, + }); + + expect(writable).toEqual({ name: 'Acme' }); + }); + + it('skips fields without a registered check rather than overwriting them', () => { + const writable = pickWritableStandard({ + standard: { extra: 'value' }, + current: { extra: 'already here' }, + emptyChecks: {}, + }); + + expect(writable).toEqual({}); + }); + + it('overwrites a non-empty value when overrideExistingValues is set, still skipping unregistered fields', () => { + const writable = pickWritableStandard({ + standard: { name: 'Acme', extra: 'value' }, + current: { name: 'Existing', extra: 'already here' }, + emptyChecks: { name: isEmptyText }, + overrideExistingValues: true, + }); + + expect(writable).toEqual({ name: 'Acme' }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/post-pdl-enrich.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/post-pdl-enrich.spec.ts new file mode 100644 index 0000000000000..2080c91e7dfc2 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/post-pdl-enrich.spec.ts @@ -0,0 +1,192 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { postPdlBulkEnrich } from 'src/logic-functions/utils/post-pdl-enrich'; + +type FetchResponse = { + status: number; + ok: boolean; + json: () => Promise; +}; + +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('postPdlBulkEnrich', () => { + beforeEach(() => { + process.env.PDL_API_KEY = 'secret-key'; + }); + + afterEach(() => { + vi.unstubAllGlobals(); + delete process.env.PDL_API_KEY; + }); + + it('sends one request wrapping every record under requests[].params', async () => { + const fetchMock = stubFetch(buildResponse(200, [{ status: 200, data: {} }])); + + await postPdlBulkEnrich({ path: '/person/bulk', requests: [{ 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/bulk'); + expect(JSON.parse(init.body as string)).toEqual({ + requests: [{ params: { email: 'a@b.com' } }], + }); + }); + + it('maps each response item to its aligned outcome', async () => { + stubFetch( + buildResponse(200, [ + { status: 200, likelihood: 8, data: { id: 'a' } }, + { status: 404 }, + { status: 500, error: { message: 'boom' } }, + ]), + ); + + const results = await postPdlBulkEnrich({ + path: '/person/bulk', + requests: [ + { email: 'a@b.com' }, + { email: 'b@b.com' }, + { email: 'c@b.com' }, + ], + }); + + expect(results).toEqual([ + { outcome: 'matched', httpStatus: 200, likelihood: 8, data: { id: 'a' } }, + { outcome: 'not_found', httpStatus: 404 }, + { outcome: 'error', httpStatus: 500, message: 'boom' }, + ]); + }); + + it('matches a 200 company item whose fields are at the top level (no data envelope)', async () => { + stubFetch( + buildResponse(200, [{ status: 200, likelihood: 6, id: 'c1', name: 'Acme' }]), + ); + + const results = await postPdlBulkEnrich({ + path: '/company/enrich/bulk', + requests: [{ name: 'Acme' }], + }); + + expect(results[0]).toEqual({ + outcome: 'matched', + httpStatus: 200, + likelihood: 6, + data: { id: 'c1', name: 'Acme' }, + }); + }); + + it('reads the responses envelope when the body is not a bare array', async () => { + stubFetch( + buildResponse(200, { responses: [{ status: 200, data: { id: 'a' } }] }), + ); + + const results = await postPdlBulkEnrich({ + path: '/person/bulk', + requests: [{ email: 'a@b.com' }], + }); + + expect(results[0]).toMatchObject({ outcome: 'matched', data: { id: 'a' } }); + }); + + it('fails the whole batch when the result count does not match the request count', async () => { + stubFetch(buildResponse(200, [{ status: 200, data: { id: 'a' } }])); + + const results = await postPdlBulkEnrich({ + path: '/person/bulk', + requests: [{ email: 'a@b.com' }, { email: 'b@b.com' }], + }); + + expect(results).toEqual([ + { + outcome: 'error', + httpStatus: 200, + message: + 'People Data Labs returned 1 results for 2 requests (HTTP 200).', + }, + { + outcome: 'error', + httpStatus: 200, + message: + 'People Data Labs returned 1 results for 2 requests (HTTP 200).', + }, + ]); + }); + + it('fails every record when the whole call is a non-2xx response', async () => { + stubFetch(buildResponse(401, { error: { message: 'unauthorized' } })); + + const results = await postPdlBulkEnrich({ + path: '/person/bulk', + requests: [{ email: 'a@b.com' }, { email: 'b@b.com' }], + }); + + expect(results).toEqual([ + { outcome: 'error', httpStatus: 401, message: 'unauthorized' }, + { outcome: 'error', httpStatus: 401, message: 'unauthorized' }, + ]); + }); + + it('fails every record when fetch throws', async () => { + stubFetch(new Error('network down')); + + const results = await postPdlBulkEnrich({ + path: '/person/bulk', + requests: [{ email: 'a@b.com' }], + }); + + expect(results).toEqual([ + { + outcome: 'error', + httpStatus: 0, + message: 'PDL request failed: network down', + }, + ]); + }); + + it('fails every record on a non-JSON response', async () => { + stubFetch(buildResponse(200, null, true)); + + const results = await postPdlBulkEnrich({ + path: '/person/bulk', + requests: [{ email: 'a@b.com' }], + }); + + expect(results).toEqual([ + { + outcome: 'error', + httpStatus: 200, + message: 'PDL returned a non-JSON response (HTTP 200).', + }, + ]); + }); + + it('returns an empty array without calling fetch for no records', async () => { + const fetchMock = stubFetch(buildResponse(200, [])); + + expect(await postPdlBulkEnrich({ path: '/person/bulk', requests: [] })).toEqual( + [], + ); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/read-companies.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/read-companies.spec.ts new file mode 100644 index 0000000000000..b4402bd9e530b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/read-companies.spec.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; + +import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock'; +import { readCompanies } from 'src/logic-functions/utils/read-companies'; + +type QueryRequest = { + companies: { __args: { filter: { id: { in: string[] } }; first: number } }; +}; + +describe('readCompanies', () => { + it('queries companies by an id-in filter and returns the matching nodes', async () => { + const nodes = [{ id: 'c1' }, { id: 'c2' }]; + let capturedRequest: unknown; + const client = createCoreApiClientMock({ + queryResult: (request: unknown) => { + capturedRequest = request; + + return { companies: { edges: nodes.map((node) => ({ node })) } }; + }, + }); + + const result = await readCompanies({ client, recordIds: ['c1', 'c2'] }); + + expect(result).toEqual(nodes); + expect((capturedRequest as QueryRequest).companies.__args.filter).toEqual({ + id: { in: ['c1', 'c2'] }, + }); + }); + + it('returns an empty array without querying when there are no ids', async () => { + const client = createCoreApiClientMock({ + queryResult: () => { + throw new Error('should not query'); + }, + }); + + expect(await readCompanies({ client, recordIds: [] })).toEqual([]); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/read-people.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/read-people.spec.ts new file mode 100644 index 0000000000000..6bc92d7d81ef2 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/read-people.spec.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; + +import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock'; +import { readPeople } from 'src/logic-functions/utils/read-people'; + +type QueryRequest = { + people: { __args: { filter: { id: { in: string[] } }; first: number } }; +}; + +describe('readPeople', () => { + it('queries people by an id-in filter and returns the matching nodes', async () => { + const nodes = [{ id: 'p1' }, { id: 'p2' }]; + let capturedRequest: unknown; + const client = createCoreApiClientMock({ + queryResult: (request: unknown) => { + capturedRequest = request; + + return { people: { edges: nodes.map((node) => ({ node })) } }; + }, + }); + + const result = await readPeople({ client, recordIds: ['p1', 'p2'] }); + + expect(result).toEqual(nodes); + expect((capturedRequest as QueryRequest).people.__args.filter).toEqual({ + id: { in: ['p1', 'p2'] }, + }); + expect((capturedRequest as QueryRequest).people.__args.first).toBe(2); + }); + + it('returns an empty array without querying when there are no ids', async () => { + const client = createCoreApiClientMock({ + queryResult: () => { + throw new Error('should not query'); + }, + }); + + expect(await readPeople({ client, recordIds: [] })).toEqual([]); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/resolve-logic-function-id.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/resolve-logic-function-id.spec.ts new file mode 100644 index 0000000000000..0c6f4ce8d6251 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/resolve-logic-function-id.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveLogicFunctionId } from 'src/logic-functions/utils/resolve-logic-function-id'; + +describe('resolveLogicFunctionId', () => { + const logicFunctions = [ + { id: 'id-a', universalIdentifier: 'uid-a' }, + { id: 'id-b', universalIdentifier: 'uid-b' }, + ]; + + it('returns the id matching the universal identifier', () => { + expect( + resolveLogicFunctionId({ logicFunctions, universalIdentifier: 'uid-b' }), + ).toBe('id-b'); + }); + + it('returns undefined when no logic function matches', () => { + expect( + resolveLogicFunctionId({ + logicFunctions, + universalIdentifier: 'uid-missing', + }), + ).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/run-batch-enrichment.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/run-batch-enrichment.spec.ts new file mode 100644 index 0000000000000..24dbdf600ffc8 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/run-batch-enrichment.spec.ts @@ -0,0 +1,395 @@ +import { describe, expect, it, vi } from 'vitest'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { runBatchEnrichment } from 'src/logic-functions/utils/run-batch-enrichment'; +import { type BatchEnrichmentAdapter } from 'src/types/batch-enrichment-adapter'; +import { type PdlEnrichResult } from 'src/types/pdl-enrich-result'; + +type FakeNode = { + id: string; + hasIdentifier: boolean; +}; +type FakeData = { id: string }; +type FakeParams = { id: string }; + +type BuildMatchedDataArgs = Parameters< + BatchEnrichmentAdapter['buildMatchedData'] +>[0]; + +type RecordConfig = { + id: string; + exists?: boolean; + hasIdentifier?: boolean; + outcome?: PdlEnrichResult; + updateFails?: boolean; + buildFails?: boolean; +}; + +const CLIENT = {} as CoreApiClient; + +const buildHarness = (configs: RecordConfig[]) => { + const byId = new Map(configs.map((config) => [config.id, config])); + + const readRecords = vi.fn( + async ({ + recordIds, + }: { + client: CoreApiClient; + recordIds: string[]; + }): Promise => + recordIds + .map((id) => byId.get(id)) + .filter( + (config): config is RecordConfig => + isPresent(config) && config.exists !== false, + ) + .map((config) => ({ + id: config.id, + hasIdentifier: config.hasIdentifier !== false, + })), + ); + + const enrichBatch = vi.fn( + async (params: FakeParams[]): Promise[]> => + params.map( + (param) => + byId.get(param.id)?.outcome ?? { + outcome: 'matched', + httpStatus: 200, + data: { id: param.id }, + }, + ), + ); + + const updateOne = vi.fn( + async ({ + recordId, + }: { + client: CoreApiClient; + recordId: string; + data: Record; + }) => { + if (byId.get(recordId)?.updateFails === true) { + throw new Error('update failed'); + } + }, + ); + + const updateManyStatus = vi.fn(async () => undefined); + + const buildMatchedData = vi.fn( + async ({ node }: BuildMatchedDataArgs): Promise> => { + if (byId.get(node.id)?.buildFails === true) { + throw new Error('build failed'); + } + + return { value: node.id }; + }, + ); + + const adapter: BatchEnrichmentAdapter = { + objectNameSingular: 'Test', + noIdentifierMessage: 'no identifier', + readRecords, + getNodeId: (node) => node.id, + extractParams: ({ node }) => + node.hasIdentifier ? { id: node.id } : undefined, + enrichBatch, + buildMatchedData, + updateOne, + updateManyStatus, + }; + + return { + adapter, + readRecords, + enrichBatch, + updateOne, + updateManyStatus, + buildMatchedData, + }; +}; + +const isPresent = (value: TValue | undefined): value is TValue => + value !== undefined; + +const records = (...ids: string[]) => ids.map((id) => ({ id })); + +describe('runBatchEnrichment', () => { + it('reads every id in one call and enriches the set in one batch', async () => { + const harness = buildHarness([{ id: 'a' }, { id: 'b' }]); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a', 'b') }, + adapter: harness.adapter, + }); + + expect(harness.readRecords).toHaveBeenCalledTimes(1); + expect(harness.readRecords).toHaveBeenCalledWith({ + client: CLIENT, + recordIds: ['a', 'b'], + }); + expect(harness.enrichBatch).toHaveBeenCalledTimes(1); + expect(harness.enrichBatch).toHaveBeenCalledWith([{ id: 'a' }, { id: 'b' }]); + expect(harness.updateOne).toHaveBeenCalledTimes(2); + expect(result).toMatchObject({ + total: 2, + matched: 2, + errored: 0, + success: true, + }); + }); + + it('writes NOT_FOUND and ERROR statuses with batched status updates', async () => { + const harness = buildHarness([ + { id: 'a', outcome: { outcome: 'not_found', httpStatus: 404 } }, + { id: 'b', outcome: { outcome: 'not_found', httpStatus: 404 } }, + { id: 'c', outcome: { outcome: 'error', httpStatus: 500, message: 'boom' } }, + ]); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a', 'b', 'c') }, + adapter: harness.adapter, + }); + + expect(harness.updateOne).not.toHaveBeenCalled(); + expect(harness.updateManyStatus).toHaveBeenCalledWith({ + client: CLIENT, + recordIds: ['c'], + data: { + pdlEnrichmentStatus: 'ERROR', + pdlLastEnrichedAt: expect.any(String), + }, + }); + expect(harness.updateManyStatus).toHaveBeenCalledWith({ + client: CLIENT, + recordIds: ['a', 'b'], + data: { + pdlEnrichmentStatus: 'NOT_FOUND', + pdlLastEnrichedAt: expect.any(String), + }, + }); + expect(result).toMatchObject({ notFound: 2, errored: 1, matched: 0 }); + expect(result.results.find((entry) => entry.recordId === 'c')?.error).toBe( + 'boom', + ); + }); + + it('skips no-identifier records before the PDL call', async () => { + const harness = buildHarness([{ id: 'a', hasIdentifier: false }, { id: 'b' }]); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a', 'b') }, + adapter: harness.adapter, + }); + + expect(harness.enrichBatch).toHaveBeenCalledWith([{ id: 'b' }]); + expect(result).toMatchObject({ skipped: 1, matched: 1 }); + }); + + it('passes overrideExistingValues through to buildMatchedData', async () => { + const harness = buildHarness([{ id: 'a' }]); + + await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a'), overrideExistingValues: true }, + adapter: harness.adapter, + }); + + expect(harness.buildMatchedData).toHaveBeenCalledWith( + expect.objectContaining({ overrideExistingValues: true }), + ); + }); + + it('defaults overrideExistingValues to false when omitted', async () => { + const harness = buildHarness([{ id: 'a' }]); + + await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a') }, + adapter: harness.adapter, + }); + + expect(harness.buildMatchedData).toHaveBeenCalledWith( + expect.objectContaining({ overrideExistingValues: false }), + ); + }); + + it('marks missing records as ERROR without enriching them', async () => { + const harness = buildHarness([{ id: 'a' }, { id: 'b', exists: false }]); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a', 'b') }, + adapter: harness.adapter, + }); + + expect(harness.enrichBatch).toHaveBeenCalledWith([{ id: 'a' }]); + expect(result.results.find((entry) => entry.recordId === 'b')).toMatchObject({ + status: 'ERROR', + error: 'Test b not found', + }); + expect(result).toMatchObject({ matched: 1, errored: 1 }); + }); + + it('isolates a per-record matched update failure', async () => { + const harness = buildHarness([{ id: 'a' }, { id: 'b', updateFails: true }]); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a', 'b') }, + adapter: harness.adapter, + }); + + expect( + result.results.find((entry) => entry.recordId === 'a')?.status, + ).toBe('MATCHED'); + expect(result.results.find((entry) => entry.recordId === 'b')).toMatchObject({ + status: 'ERROR', + error: 'update failed', + }); + expect(harness.updateManyStatus).toHaveBeenCalledWith({ + client: CLIENT, + recordIds: ['b'], + data: { + pdlEnrichmentStatus: 'ERROR', + pdlLastEnrichedAt: expect.any(String), + }, + }); + }); + + it('marks all attempted records as ERROR and writes the status when the batch PDL call rejects', async () => { + const harness = buildHarness([{ id: 'a' }, { id: 'b' }]); + harness.enrichBatch.mockRejectedValueOnce(new Error('pdl down')); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a', 'b') }, + adapter: harness.adapter, + }); + + expect(result).toMatchObject({ errored: 2, matched: 0 }); + expect(result.results[0].error).toBe('pdl down'); + expect(harness.updateOne).not.toHaveBeenCalled(); + expect(harness.updateManyStatus).toHaveBeenCalledWith({ + client: CLIENT, + recordIds: ['a', 'b'], + data: { + pdlEnrichmentStatus: 'ERROR', + pdlLastEnrichedAt: expect.any(String), + }, + }); + }); + + it('isolates a record whose matched-data build throws', async () => { + const harness = buildHarness([{ id: 'a' }, { id: 'b', buildFails: true }]); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a', 'b') }, + adapter: harness.adapter, + }); + + expect( + result.results.find((entry) => entry.recordId === 'a')?.status, + ).toBe('MATCHED'); + expect(result.results.find((entry) => entry.recordId === 'b')).toMatchObject({ + status: 'ERROR', + error: 'build failed', + }); + expect(harness.updateOne).toHaveBeenCalledTimes(1); + expect(harness.updateManyStatus).toHaveBeenCalledWith({ + client: CLIENT, + recordIds: ['b'], + data: { + pdlEnrichmentStatus: 'ERROR', + pdlLastEnrichedAt: expect.any(String), + }, + }); + }); + + it('produces exactly one result per record with counts summing to the total', async () => { + const harness = buildHarness([ + { id: 'matched' }, + { id: 'notfound', outcome: { outcome: 'not_found', httpStatus: 404 } }, + { id: 'errored', outcome: { outcome: 'error', httpStatus: 500, message: 'x' } }, + { id: 'skipped', hasIdentifier: false }, + { id: 'missing', exists: false }, + ]); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { + records: records('matched', 'notfound', 'errored', 'skipped', 'missing'), + }, + adapter: harness.adapter, + }); + + const recordIds = result.results.map((entry) => entry.recordId); + expect(new Set(recordIds).size).toBe(recordIds.length); + expect(result.results).toHaveLength(result.total); + expect( + result.matched + result.notFound + result.skipped + result.errored, + ).toBe(result.total); + expect(result).toMatchObject({ + total: 5, + matched: 1, + notFound: 1, + errored: 2, + skipped: 1, + success: false, + }); + }); + + it('deduplicates record ids', async () => { + const harness = buildHarness([{ id: 'a' }]); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: records('a', 'a') }, + adapter: harness.adapter, + }); + + expect(harness.enrichBatch).toHaveBeenCalledWith([{ id: 'a' }]); + expect(result.total).toBe(1); + }); + + it('chunks large id sets into separate read and PDL calls', async () => { + const ids = Array.from({ length: 150 }, (_unused, index) => `r${index}`); + const harness = buildHarness(ids.map((id) => ({ id }))); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: ids.map((id) => ({ id })) }, + adapter: harness.adapter, + }); + + expect(harness.readRecords).toHaveBeenCalledTimes(2); + expect(harness.enrichBatch).toHaveBeenCalledTimes(2); + expect(result.matched).toBe(150); + }); + + it('returns an empty summary when there are no records', async () => { + const harness = buildHarness([]); + + const result = await runBatchEnrichment({ + client: CLIENT, + input: { records: [] }, + adapter: harness.adapter, + }); + + expect(harness.readRecords).not.toHaveBeenCalled(); + expect(result).toEqual({ + success: true, + total: 0, + matched: 0, + notFound: 0, + skipped: 0, + errored: 0, + results: [], + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/salary-transform.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/salary-transform.spec.ts new file mode 100644 index 0000000000000..0c691e8c2583e --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/salary-transform.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; + +import { salaryTransform } from 'src/logic-functions/utils/salary-transform'; + +describe('salaryTransform', () => { + it('maps PDL salary ranges to option values', () => { + expect(salaryTransform('<20,000')).toBe('UNDER_20000'); + expect(salaryTransform('45,000-55,000')).toBe('FROM_45000_TO_55000'); + expect(salaryTransform('>250,000')).toBe('OVER_250000'); + }); + + it('returns undefined for an unknown range', () => { + expect(salaryTransform('weird')).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/seed-enrichment-workflow.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/seed-enrichment-workflow.spec.ts new file mode 100644 index 0000000000000..4f6624d926c01 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/seed-enrichment-workflow.spec.ts @@ -0,0 +1,136 @@ +import { describe, expect, it } from 'vitest'; + +import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock'; +import { seedEnrichmentWorkflow } from 'src/logic-functions/utils/seed-enrichment-workflow'; +import { type EnrichmentWorkflowSeed } from 'src/types/enrichment-workflow-seed'; + +const SEED: EnrichmentWorkflowSeed = { + objectNameSingular: 'company', + workflowName: 'Enrich companies with People Data Labs', + triggerName: 'When companies are selected', + icon: 'IconSparkles', + stepName: 'Enrich with People Data Labs', + logicFunctionUniversalIdentifier: 'lf-universal-id', + logicFunctionInput: { records: '{{trigger.companies}}' }, +}; + +type AnyRequest = Record; + +describe('seedEnrichmentWorkflow', () => { + it('creates, configures and activates a new workflow', async () => { + const mutations: AnyRequest[] = []; + + const client = createCoreApiClientMock({ + queryResult: (request: unknown) => { + const req = request as AnyRequest; + if ('workflows' in req) { + return { workflows: { edges: [] } }; + } + if ('workflowVersions' in req) { + return { + workflowVersions: { + edges: [{ node: { id: 'version-1', status: 'DRAFT' } }], + }, + }; + } + return {}; + }, + mutationResult: (request: unknown) => + 'createWorkflow' in (request as AnyRequest) + ? { createWorkflow: { id: 'workflow-1' } } + : {}, + onMutation: (request) => mutations.push(request as AnyRequest), + }); + + const result = await seedEnrichmentWorkflow({ + client, + logicFunctionId: 'logic-function-1', + seed: SEED, + }); + + expect(result).toEqual({ + objectNameSingular: 'company', + workflowName: SEED.workflowName, + status: 'created', + workflowId: 'workflow-1', + }); + + const createRequest = mutations.find( + (request) => 'createWorkflow' in request, + ); + expect(createRequest?.createWorkflow.__args.data).toEqual({ + name: SEED.workflowName, + }); + + const updateRequest = mutations.find( + (request) => 'updateWorkflowVersion' in request, + ); + const { id, data } = updateRequest?.updateWorkflowVersion.__args ?? {}; + expect(id).toBe('version-1'); + + expect(data.trigger.type).toBe('MANUAL'); + expect(data.trigger.settings.availability).toEqual({ + type: 'BULK_RECORDS', + objectNameSingular: 'company', + }); + expect(data.trigger.settings.objectType).toBe('company'); + expect(data.trigger.nextStepIds).toEqual([]); + expect(data.steps).toBeUndefined(); + + const createStepRequest = mutations.find( + (request) => 'createWorkflowVersionStep' in request, + ); + const createStepInput = + createStepRequest?.createWorkflowVersionStep.__args.input; + expect(createStepInput.workflowVersionId).toBe('version-1'); + expect(createStepInput.stepType).toBe('LOGIC_FUNCTION'); + expect(createStepInput.parentStepId).toBe('trigger'); + expect(createStepInput.defaultSettings.input.logicFunctionId).toBe( + 'logic-function-1', + ); + + const updateStepRequest = mutations.find( + (request) => 'updateWorkflowVersionStep' in request, + ); + const updateStepInput = + updateStepRequest?.updateWorkflowVersionStep.__args.input; + expect(updateStepInput.workflowVersionId).toBe('version-1'); + expect(updateStepInput.step.id).toBe(createStepInput.id); + expect(updateStepInput.step.type).toBe('LOGIC_FUNCTION'); + expect(updateStepInput.step.settings.input.logicFunctionId).toBe( + 'logic-function-1', + ); + expect(updateStepInput.step.settings.input.logicFunctionInput).toEqual({ + records: '{{trigger.companies}}', + }); + + const activateRequest = mutations.find( + (request) => 'activateWorkflowVersion' in request, + ); + expect( + activateRequest?.activateWorkflowVersion.__args.workflowVersionId, + ).toBe('version-1'); + }); + + it('skips creation when a workflow with the same name already exists', async () => { + const mutations: AnyRequest[] = []; + + const client = createCoreApiClientMock({ + queryResult: (request: unknown) => + 'workflows' in (request as AnyRequest) + ? { workflows: { edges: [{ node: { id: 'existing-1' } }] } } + : {}, + onMutation: (request) => mutations.push(request as AnyRequest), + }); + + const result = await seedEnrichmentWorkflow({ + client, + logicFunctionId: 'logic-function-1', + seed: SEED, + }); + + expect(result.status).toBe('skipped'); + expect(result.workflowId).toBe('existing-1'); + expect(mutations).toHaveLength(0); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/size-transform.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/size-transform.spec.ts new file mode 100644 index 0000000000000..e3dd3bb8c83e4 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/size-transform.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; + +import { sizeTransform } from 'src/logic-functions/utils/size-transform'; + +describe('sizeTransform', () => { + it('maps PDL size ranges to option values', () => { + expect(sizeTransform('1-10')).toBe('ONE_TO_TEN'); + expect(sizeTransform('10001+')).toBe('TEN_THOUSAND_ONE_PLUS'); + }); + + it('tolerates spacing variants', () => { + expect(sizeTransform('51 - 200')).toBe('FIFTY_ONE_TO_TWO_HUNDRED'); + }); + + it('returns undefined for an unknown range', () => { + expect(sizeTransform('weird')).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-json-array.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-json-array.spec.ts new file mode 100644 index 0000000000000..65161c5951f00 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-json-array.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { toJsonArray } from 'src/logic-functions/utils/to-json-array'; + +describe('toJsonArray', () => { + it('returns a non-empty array unchanged', () => { + const value = [{ a: 1 }]; + + expect(toJsonArray(value)).toBe(value); + }); + + it('returns undefined for empty arrays and non-arrays', () => { + expect(toJsonArray([])).toBeUndefined(); + expect(toJsonArray({ a: 1 })).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-json-object.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-json-object.spec.ts new file mode 100644 index 0000000000000..b50dad3447213 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-json-object.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; + +import { toJsonObject } from 'src/logic-functions/utils/to-json-object'; + +describe('toJsonObject', () => { + it('returns a non-empty plain object unchanged', () => { + const value = { country: { us: 10 } }; + + expect(toJsonObject(value)).toBe(value); + }); + + it('returns undefined for empty objects, arrays and non-objects', () => { + expect(toJsonObject({})).toBeUndefined(); + expect(toJsonObject([1, 2])).toBeUndefined(); + expect(toJsonObject('x')).toBeUndefined(); + expect(toJsonObject(null)).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-number.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-number.spec.ts new file mode 100644 index 0000000000000..686bba3c38835 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-number.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { toNumber } from 'src/logic-functions/utils/to-number'; + +describe('toNumber', () => { + it('returns finite numbers including zero', () => { + expect(toNumber(0)).toBe(0); + expect(toNumber(120)).toBe(120); + }); + + it('returns undefined for non-finite numbers and non-numbers', () => { + expect(toNumber(Number.NaN)).toBeUndefined(); + expect(toNumber(Number.POSITIVE_INFINITY)).toBeUndefined(); + expect(toNumber('120')).toBeUndefined(); + expect(toNumber(null)).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-string-array.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-string-array.spec.ts new file mode 100644 index 0000000000000..3b0fdaf2105c4 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-string-array.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { toStringArray } from 'src/logic-functions/utils/to-string-array'; + +describe('toStringArray', () => { + it('cleans, trims and dedupes string entries', () => { + expect(toStringArray([' a ', 'b', 'a', '', 3, null])).toEqual(['a', 'b']); + }); + + it('returns undefined for a non-array', () => { + expect(toStringArray('a')).toBeUndefined(); + }); + + it('returns undefined when nothing usable remains', () => { + expect(toStringArray(['', ' ', null])).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-text.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-text.spec.ts new file mode 100644 index 0000000000000..a386c8473d009 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/to-text.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; + +import { toText } from 'src/logic-functions/utils/to-text'; + +describe('toText', () => { + it('trims and returns a non-empty string', () => { + expect(toText(' hello ')).toBe('hello'); + }); + + it('returns undefined for an empty or whitespace string', () => { + expect(toText('')).toBeUndefined(); + expect(toText(' ')).toBeUndefined(); + }); + + it('returns undefined for non-string values', () => { + expect(toText(42)).toBeUndefined(); + expect(toText(null)).toBeUndefined(); + expect(toText(undefined)).toBeUndefined(); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-companies-status.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-companies-status.spec.ts new file mode 100644 index 0000000000000..356807c10950c --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-companies-status.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock'; +import { updateCompaniesStatus } from 'src/logic-functions/utils/update-companies-status'; + +describe('updateCompaniesStatus', () => { + it('updates many companies via an id-in filter', async () => { + let request: Record | undefined; + const client = createCoreApiClientMock({ + onMutation: (received) => { + request = received as Record; + }, + }); + + await updateCompaniesStatus({ + client, + recordIds: ['c1', 'c2'], + data: { + pdlEnrichmentStatus: 'ERROR', + }, + }); + + expect(request).toMatchObject({ + updateCompanies: { + __args: { + filter: { id: { in: ['c1', 'c2'] } }, + data: { pdlEnrichmentStatus: 'ERROR' }, + }, + }, + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-company-record.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-company-record.spec.ts new file mode 100644 index 0000000000000..3e87937d776cf --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-company-record.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock'; +import { updateCompanyRecord } from 'src/logic-functions/utils/update-company-record'; + +describe('updateCompanyRecord', () => { + it('updates a single company by id', async () => { + let request: Record | undefined; + const client = createCoreApiClientMock({ + onMutation: (received) => { + request = received as Record; + }, + }); + + await updateCompanyRecord({ + client, + recordId: 'c1', + data: { name: 'Acme Corp' }, + }); + + expect(request).toMatchObject({ + updateCompany: { __args: { id: 'c1', data: { name: 'Acme Corp' } } }, + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-people-status.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-people-status.spec.ts new file mode 100644 index 0000000000000..59b759b15e0b2 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-people-status.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock'; +import { updatePeopleStatus } from 'src/logic-functions/utils/update-people-status'; + +describe('updatePeopleStatus', () => { + it('updates many people via an id-in filter', async () => { + let request: Record | undefined; + const client = createCoreApiClientMock({ + onMutation: (received) => { + request = received as Record; + }, + }); + + await updatePeopleStatus({ + client, + recordIds: ['p1', 'p2'], + data: { + pdlEnrichmentStatus: 'NOT_FOUND', + }, + }); + + expect(request).toMatchObject({ + updatePeople: { + __args: { + filter: { id: { in: ['p1', 'p2'] } }, + data: { pdlEnrichmentStatus: 'NOT_FOUND' }, + }, + }, + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-person-record.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-person-record.spec.ts new file mode 100644 index 0000000000000..e3348a95e63db --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/__tests__/update-person-record.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { createCoreApiClientMock } from 'src/logic-functions/__mocks__/create-core-api-client-mock'; +import { updatePersonRecord } from 'src/logic-functions/utils/update-person-record'; + +describe('updatePersonRecord', () => { + it('updates a single person by id', async () => { + let request: Record | undefined; + const client = createCoreApiClientMock({ + onMutation: (received) => { + request = received as Record; + }, + }); + + await updatePersonRecord({ + client, + recordId: 'p1', + data: { jobTitle: 'CEO' }, + }); + + expect(request).toMatchObject({ + updatePerson: { __args: { id: 'p1', data: { jobTitle: 'CEO' } } }, + }); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/aggregate-bulk-enrich-result.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/aggregate-bulk-enrich-result.ts new file mode 100644 index 0000000000000..f249c13d21093 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/aggregate-bulk-enrich-result.ts @@ -0,0 +1,21 @@ +import { type BulkEnrichResult } from 'src/types/bulk-enrich-result'; +import { type EnrichResult } from 'src/types/enrich-result'; + +export const aggregateBulkEnrichResult = ( + results: EnrichResult[], +): BulkEnrichResult => { + const countByStatus = (status: EnrichResult['status']): number => + results.filter((result) => result.status === status).length; + + const errored = countByStatus('ERROR'); + + return { + success: errored === 0, + total: results.length, + matched: countByStatus('MATCHED'), + notFound: countByStatus('NOT_FOUND'), + skipped: countByStatus('SKIPPED'), + errored, + results, + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-address.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-address.ts new file mode 100644 index 0000000000000..01ce32ba835f4 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-address.ts @@ -0,0 +1,40 @@ +import { toText } from 'src/logic-functions/utils/to-text'; +import { parseGeo } from 'src/logic-functions/utils/parse-geo'; +import { type AddressParts } from 'src/types/address-parts'; +import { type AddressValue } from 'src/types/address-value'; +import { isDefined } from 'src/utils/is-defined'; + +export const buildAddress = (parts: AddressParts): AddressValue | undefined => { + const street1 = toText(parts.street1); + const street2 = toText(parts.street2); + const city = toText(parts.city); + const postcode = toText(parts.postcode); + const state = toText(parts.state); + const country = toText(parts.country); + const { lat, lng } = parseGeo(parts.geo); + + const hasAnyValue = + isDefined(street1) || + isDefined(street2) || + isDefined(city) || + isDefined(postcode) || + isDefined(state) || + isDefined(country) || + isDefined(lat) || + isDefined(lng); + + if (!hasAnyValue) { + return undefined; + } + + return { + addressStreet1: street1 ?? '', + addressStreet2: street2 ?? '', + addressCity: city ?? '', + addressPostcode: postcode ?? '', + addressState: state ?? '', + addressCountry: country ?? '', + addressLat: lat, + addressLng: lng, + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-allowed-values.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-allowed-values.ts new file mode 100644 index 0000000000000..4f3017ad02f26 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-allowed-values.ts @@ -0,0 +1,5 @@ +import { type SelectOptionMeta } from 'src/types/select-option-meta'; + +export const buildAllowedValues = ( + options: readonly SelectOptionMeta[], +): Set => new Set(options.map((option) => option.value)); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-bulk-records-trigger.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-bulk-records-trigger.ts new file mode 100644 index 0000000000000..a8309ddcd8a7b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-bulk-records-trigger.ts @@ -0,0 +1,25 @@ +import { type WorkflowBulkRecordsTrigger } from 'src/types/workflow-bulk-records-trigger'; + +export const buildBulkRecordsTrigger = ({ + objectNameSingular, + name, + icon, +}: { + objectNameSingular: string; + name: string; + icon: string; +}): WorkflowBulkRecordsTrigger => ({ + name, + type: 'MANUAL', + settings: { + objectType: objectNameSingular, + availability: { + type: 'BULK_RECORDS', + objectNameSingular, + }, + outputSchema: {}, + icon, + isPinned: false, + }, + nextStepIds: [], +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-company-create-data.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-company-create-data.ts new file mode 100644 index 0000000000000..3ec68bf4823ad --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-company-create-data.ts @@ -0,0 +1,35 @@ +import { INDUSTRY_OPTIONS } from 'src/constants/industry-options'; +import { SIZE_OPTIONS } from 'src/constants/size-options'; +import { buildAllowedValues } from 'src/logic-functions/utils/build-allowed-values'; +import { buildLinks } from 'src/logic-functions/utils/build-links'; +import { normalizeDomain } from 'src/logic-functions/utils/normalize-domain'; +import { normalizeLinkedinUrl } from 'src/logic-functions/utils/normalize-linkedin-url'; +import { pickSelect } from 'src/logic-functions/utils/pick-select'; +import { sizeTransform } from 'src/logic-functions/utils/size-transform'; +import { toText } from 'src/logic-functions/utils/to-text'; +import { type PdlPersonData } from 'src/types/pdl-person-data'; +import { pruneUndefined } from 'src/utils/prune-undefined'; + +const INDUSTRY_VALUES = buildAllowedValues(INDUSTRY_OPTIONS); +const SIZE_VALUES = buildAllowedValues(SIZE_OPTIONS); + +export const buildCompanyCreateData = ( + personData: PdlPersonData, +): Record => + pruneUndefined({ + name: toText(personData.job_company_name), + domainName: buildLinks({ url: normalizeDomain(personData.job_company_website) }), + linkedinLink: buildLinks({ + url: normalizeLinkedinUrl(personData.job_company_linkedin_url), + }), + pdlId: toText(personData.job_company_id), + pdlIndustry: pickSelect({ + raw: personData.job_company_industry, + allowedValues: INDUSTRY_VALUES, + }), + pdlSizeRange: pickSelect({ + raw: personData.job_company_size, + allowedValues: SIZE_VALUES, + transform: sizeTransform, + }), + }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-company-match-keys.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-company-match-keys.ts new file mode 100644 index 0000000000000..6266001219eb3 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-company-match-keys.ts @@ -0,0 +1,16 @@ +import { normalizeDomain } from 'src/logic-functions/utils/normalize-domain'; +import { normalizeLinkedinUrl } from 'src/logic-functions/utils/normalize-linkedin-url'; +import { toText } from 'src/logic-functions/utils/to-text'; +import { type CompanyMatchKeys } from 'src/types/company-match-keys'; +import { type PdlPersonData } from 'src/types/pdl-person-data'; +import { pruneUndefined } from 'src/utils/prune-undefined'; + +export const buildCompanyMatchKeys = ( + personData: PdlPersonData, +): CompanyMatchKeys => + pruneUndefined({ + pdlId: toText(personData.job_company_id), + website: normalizeDomain(personData.job_company_website), + linkedinUrl: normalizeLinkedinUrl(personData.job_company_linkedin_url), + name: toText(personData.job_company_name), + }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-company-matched-data.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-company-matched-data.ts new file mode 100644 index 0000000000000..aada6085cbc37 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-company-matched-data.ts @@ -0,0 +1,43 @@ +import { isEmptyAddress } from 'src/logic-functions/utils/is-empty-address'; +import { isEmptyLinks } from 'src/logic-functions/utils/is-empty-links'; +import { isEmptyText } from 'src/logic-functions/utils/is-empty-text'; +import { mapCompany } from 'src/logic-functions/utils/map-company'; +import { pickWritableStandard } from 'src/logic-functions/utils/pick-writable-standard'; +import { type CompanyNode } from 'src/types/company-node'; +import { type PdlCompanyData } from 'src/types/pdl-company-data'; +import { pruneUndefined } from 'src/utils/prune-undefined'; + +const COMPANY_EMPTY_CHECKS = { + name: isEmptyText, + domainName: isEmptyLinks, + linkedinLink: isEmptyLinks, + address: isEmptyAddress, +}; + +export const buildCompanyMatchedData = async ({ + node, + outcome, + enrichedAt, + overrideExistingValues, +}: { + node: CompanyNode; + outcome: { data: PdlCompanyData }; + enrichedAt: string; + overrideExistingValues: boolean; +}): Promise> => { + const mapped = mapCompany(outcome.data); + const writableStandard = pickWritableStandard({ + standard: mapped.standard, + current: node as unknown as Record, + emptyChecks: COMPANY_EMPTY_CHECKS, + overrideExistingValues, + }); + + return pruneUndefined({ + ...writableStandard, + ...mapped.pdl, + pdlRawPayload: outcome.data, + pdlLastEnrichedAt: enrichedAt, + pdlEnrichmentStatus: 'MATCHED', + }); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-currency-from-usd.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-currency-from-usd.ts new file mode 100644 index 0000000000000..56800bfae806d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-currency-from-usd.ts @@ -0,0 +1,16 @@ +import { isNumber } from '@sniptt/guards'; + +import { type CurrencyValue } from 'src/types/currency-value'; + +export const buildCurrencyFromUsd = ( + amount: unknown, +): CurrencyValue | undefined => { + if (!isNumber(amount) || !Number.isFinite(amount)) { + return undefined; + } + + return { + amountMicros: Math.round(amount * 1_000_000), + currencyCode: 'USD', + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-emails.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-emails.ts new file mode 100644 index 0000000000000..4474c0c771674 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-emails.ts @@ -0,0 +1,34 @@ +import { isNonEmptyArray } from '@sniptt/guards'; + +import { toText } from 'src/logic-functions/utils/to-text'; +import { type EmailsValue } from 'src/types/emails-value'; +import { isDefined } from 'src/utils/is-defined'; + +export const buildEmails = ( + candidates: (string | null | undefined)[], +): EmailsValue | undefined => { + const emails: string[] = []; + const seenEmailKeys = new Set(); + + for (const candidate of candidates) { + const email = toText(candidate); + if (!isDefined(email)) { + continue; + } + + const emailKey = email.toLowerCase(); + if (!seenEmailKeys.has(emailKey)) { + seenEmailKeys.add(emailKey); + emails.push(email); + } + } + + if (!isNonEmptyArray(emails)) { + return undefined; + } + + return { + primaryEmail: emails[0], + additionalEmails: emails.length > 1 ? emails.slice(1) : null, + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-empty-tool-result.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-empty-tool-result.ts new file mode 100644 index 0000000000000..1484fd06caffe --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-empty-tool-result.ts @@ -0,0 +1,11 @@ +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: [], +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-error-result.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-error-result.ts new file mode 100644 index 0000000000000..619c7cd57ccf4 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-error-result.ts @@ -0,0 +1,18 @@ +import { type EnrichResult } from 'src/types/enrich-result'; + +export const ENRICHMENT_FAILED_MESSAGE = 'People Data Labs enrichment failed.'; + +export const buildErrorResult = ({ + recordId, + error, +}: { + recordId: string; + error: string; +}): EnrichResult => ({ + success: false, + recordId, + status: 'ERROR', + updatedFields: [], + message: ENRICHMENT_FAILED_MESSAGE, + error, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-full-name.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-full-name.ts new file mode 100644 index 0000000000000..98100df8cb91b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-full-name.ts @@ -0,0 +1,38 @@ +import { capitalizeName } from 'src/logic-functions/utils/capitalize-name'; +import { toText } from 'src/logic-functions/utils/to-text'; +import { type FullNameValue } from 'src/types/full-name-value'; +import { isDefined } from 'src/utils/is-defined'; + +const WHITESPACE_REGEX = /\s+/; + +export const buildFullName = ({ + firstName, + lastName, + fullName, +}: { + firstName: unknown; + lastName: unknown; + fullName: unknown; +}): FullNameValue | undefined => { + const first = toText(firstName); + const last = toText(lastName); + + if (isDefined(first) || isDefined(last)) { + return { + firstName: isDefined(first) ? capitalizeName(first) : '', + lastName: isDefined(last) ? capitalizeName(last) : '', + }; + } + + const full = toText(fullName); + if (!isDefined(full)) { + return undefined; + } + + const [firstNameToken, ...remainingNameTokens] = full.split(WHITESPACE_REGEX); + + return { + firstName: capitalizeName(firstNameToken), + lastName: capitalizeName(remainingNameTokens.join(' ')), + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-links.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-links.ts new file mode 100644 index 0000000000000..c3901b03601fc --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-links.ts @@ -0,0 +1,23 @@ +import { toText } from 'src/logic-functions/utils/to-text'; +import { type LinksValue } from 'src/types/links-value'; +import { isDefined } from 'src/utils/is-defined'; + +export const buildLinks = ({ + url, + label, +}: { + url: unknown; + label?: unknown; +}): LinksValue | undefined => { + const primaryLinkUrl = toText(url); + + if (!isDefined(primaryLinkUrl)) { + return undefined; + } + + return { + primaryLinkUrl, + primaryLinkLabel: toText(label) ?? '', + secondaryLinks: null, + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-logic-function-step.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-logic-function-step.ts new file mode 100644 index 0000000000000..a47f0ecbe2d89 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-logic-function-step.ts @@ -0,0 +1,30 @@ +import { type WorkflowLogicFunctionStep } from 'src/types/workflow-logic-function-step'; + +export const buildLogicFunctionStep = ({ + id, + name, + logicFunctionId, + logicFunctionInput, +}: { + id: string; + name: string; + logicFunctionId: string; + logicFunctionInput: Record; +}): WorkflowLogicFunctionStep => ({ + id, + name, + type: 'LOGIC_FUNCTION', + valid: true, + settings: { + input: { + logicFunctionId, + logicFunctionInput, + }, + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: false }, + continueOnFailure: { value: false }, + }, + }, + nextStepIds: [], +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-matched-result.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-matched-result.ts new file mode 100644 index 0000000000000..0c809afa27e31 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-matched-result.ts @@ -0,0 +1,15 @@ +import { type EnrichResult } from 'src/types/enrich-result'; + +export const buildMatchedResult = ({ + recordId, + updatedFields, +}: { + recordId: string; + updatedFields: string[]; +}): EnrichResult => ({ + success: true, + recordId, + status: 'MATCHED', + updatedFields, + message: `Enriched with People Data Labs (${updatedFields.length} fields).`, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-not-found-result.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-not-found-result.ts new file mode 100644 index 0000000000000..f625c7292a676 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-not-found-result.ts @@ -0,0 +1,9 @@ +import { type EnrichResult } from 'src/types/enrich-result'; + +export const buildNotFoundResult = (recordId: string): EnrichResult => ({ + success: true, + recordId, + status: 'NOT_FOUND', + updatedFields: [], + message: 'People Data Labs returned no confident match.', +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-person-matched-data.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-person-matched-data.ts new file mode 100644 index 0000000000000..fa462d9d6ddd8 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-person-matched-data.ts @@ -0,0 +1,65 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { findOrCreateCurrentCompany } from 'src/logic-functions/utils/find-or-create-current-company'; +import { isEmptyEmails } from 'src/logic-functions/utils/is-empty-emails'; +import { isEmptyFullName } from 'src/logic-functions/utils/is-empty-full-name'; +import { isEmptyLinks } from 'src/logic-functions/utils/is-empty-links'; +import { isEmptyPhones } from 'src/logic-functions/utils/is-empty-phones'; +import { isEmptyText } from 'src/logic-functions/utils/is-empty-text'; +import { mapPerson } from 'src/logic-functions/utils/map-person'; +import { pickWritableStandard } from 'src/logic-functions/utils/pick-writable-standard'; +import { type CompanyIdByMatchKeyCache } from 'src/types/company-id-by-match-key-cache'; +import { type PdlPersonData } from 'src/types/pdl-person-data'; +import { type PersonNode } from 'src/types/person-node'; +import { isDefined } from 'src/utils/is-defined'; +import { pruneUndefined } from 'src/utils/prune-undefined'; + +const PERSON_EMPTY_CHECKS = { + name: isEmptyFullName, + emails: isEmptyEmails, + phones: isEmptyPhones, + jobTitle: isEmptyText, + linkedinLink: isEmptyLinks, +}; + +export const buildPersonMatchedData = async ({ + client, + node, + outcome, + enrichedAt, + companyIdByMatchKeyCache, + overrideExistingValues, +}: { + client: CoreApiClient; + node: PersonNode; + outcome: { likelihood?: number; data: PdlPersonData }; + enrichedAt: string; + companyIdByMatchKeyCache: CompanyIdByMatchKeyCache; + overrideExistingValues: boolean; +}): Promise> => { + const mapped = mapPerson(outcome.data); + const writableStandard = pickWritableStandard({ + standard: mapped.standard, + current: node as unknown as Record, + emptyChecks: PERSON_EMPTY_CHECKS, + overrideExistingValues, + }); + + const currentCompanyId = isDefined(node.company?.id) + ? undefined + : await findOrCreateCurrentCompany({ + client, + personData: outcome.data, + companyIdByMatchKeyCache, + }); + + return pruneUndefined({ + ...writableStandard, + ...mapped.pdl, + companyId: currentCompanyId, + pdlLikelihood: outcome.likelihood, + pdlRawPayload: outcome.data, + pdlLastEnrichedAt: enrichedAt, + pdlEnrichmentStatus: 'MATCHED', + }); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-person-name-param.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-person-name-param.ts new file mode 100644 index 0000000000000..3956ba6885045 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-person-name-param.ts @@ -0,0 +1,19 @@ +import { toText } from 'src/logic-functions/utils/to-text'; +import { isDefined } from 'src/utils/is-defined'; + +export const buildPersonNameParam = ({ + firstName, + lastName, +}: { + firstName: unknown; + lastName: unknown; +}): string | undefined => { + const first = toText(firstName); + const last = toText(lastName); + + if (!isDefined(first) || !isDefined(last)) { + return undefined; + } + + return `${first} ${last}`; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-phones.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-phones.ts new file mode 100644 index 0000000000000..16bfe6eac0fd7 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-phones.ts @@ -0,0 +1,40 @@ +import { isNonEmptyArray } from '@sniptt/guards'; + +import { toText } from 'src/logic-functions/utils/to-text'; +import { type PhonesValue } from 'src/types/phones-value'; +import { isDefined } from 'src/utils/is-defined'; + +export const buildPhones = ( + phoneCandidates: (string | null | undefined)[], +): PhonesValue | undefined => { + const uniquePhoneNumbers: string[] = []; + const seenPhoneNumbers = new Set(); + + for (const candidate of phoneCandidates) { + const phoneNumber = toText(candidate); + if (!isDefined(phoneNumber) || seenPhoneNumbers.has(phoneNumber)) { + continue; + } + + seenPhoneNumbers.add(phoneNumber); + uniquePhoneNumbers.push(phoneNumber); + } + + const [primaryPhoneNumber, ...additionalPhoneNumbers] = uniquePhoneNumbers; + if (!isDefined(primaryPhoneNumber)) { + return undefined; + } + + return { + primaryPhoneNumber, + primaryPhoneCountryCode: '', + primaryPhoneCallingCode: '', + additionalPhones: isNonEmptyArray(additionalPhoneNumbers) + ? additionalPhoneNumbers.map((phoneNumber) => ({ + number: phoneNumber, + countryCode: '', + callingCode: '', + })) + : null, + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-skipped-result.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-skipped-result.ts new file mode 100644 index 0000000000000..88dc8b46d21c2 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-skipped-result.ts @@ -0,0 +1,15 @@ +import { type EnrichResult } from 'src/types/enrich-result'; + +export const buildSkippedResult = ({ + recordId, + message, +}: { + recordId: string; + message: string; +}): EnrichResult => ({ + success: true, + recordId, + status: 'SKIPPED', + updatedFields: [], + message, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-tool-record-ids.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-tool-record-ids.ts new file mode 100644 index 0000000000000..c3d9419286bc6 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/build-tool-record-ids.ts @@ -0,0 +1,12 @@ +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)); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/capitalize-name.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/capitalize-name.ts new file mode 100644 index 0000000000000..6d963c0fbc6eb --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/capitalize-name.ts @@ -0,0 +1,8 @@ +const NAME_BOUNDARY_REGEX = /(^|[\s'-])(\p{L})/gu; + +export const capitalizeName = (value: string): string => + value.replace( + NAME_BOUNDARY_REGEX, + (_match, boundary: string, letter: string) => + `${boundary}${letter.toUpperCase()}`, + ); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/chunk.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/chunk.ts new file mode 100644 index 0000000000000..b66ebdc757ffd --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/chunk.ts @@ -0,0 +1,18 @@ +export const chunk = ({ + items, + size, +}: { + items: TItem[]; + size: number; +}): TItem[][] => { + if (size <= 0) { + return items.length > 0 ? [items] : []; + } + + const chunks: TItem[][] = []; + for (let index = 0; index < items.length; index += size) { + chunks.push(items.slice(index, index + size)); + } + + return chunks; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/enrich-chunk.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/enrich-chunk.ts new file mode 100644 index 0000000000000..36b7df3377eec --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/enrich-chunk.ts @@ -0,0 +1,259 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { buildErrorResult } from 'src/logic-functions/utils/build-error-result'; +import { buildMatchedResult } from 'src/logic-functions/utils/build-matched-result'; +import { buildNotFoundResult } from 'src/logic-functions/utils/build-not-found-result'; +import { buildSkippedResult } from 'src/logic-functions/utils/build-skipped-result'; +import { INTERNAL_BOOKKEEPING_FIELDS } from 'src/logic-functions/utils/internal-field-names'; +import { nowIso } from 'src/logic-functions/utils/now-iso'; +import { type BatchEnrichmentAdapter } from 'src/types/batch-enrichment-adapter'; +import { type BulkEnrichInput } from 'src/types/bulk-enrich-input'; +import { type CompanyIdByMatchKeyCache } from 'src/types/company-id-by-match-key-cache'; +import { type EnrichResult } from 'src/types/enrich-result'; +import { type PdlEnrichResult } from 'src/types/pdl-enrich-result'; +import { isDefined } from 'src/utils/is-defined'; +import { toErrorMessage } from 'src/utils/to-error-message'; + +const writeErrorStatusWithBackoff = async ({ + adapter, + client, + recordIds, + enrichedAt, +}: { + adapter: BatchEnrichmentAdapter; + client: CoreApiClient; + recordIds: string[]; + enrichedAt: string; +}): Promise => { + if (recordIds.length === 0) { + return; + } + + await adapter + .updateManyStatus({ + client, + recordIds, + data: { pdlEnrichmentStatus: 'ERROR', pdlLastEnrichedAt: enrichedAt }, + }) + .catch(() => undefined); +}; + +export const enrichChunk = async ({ + client, + recordIds, + input, + adapter, + resultById, + companyIdByMatchKeyCache, +}: { + client: CoreApiClient; + recordIds: string[]; + input: BulkEnrichInput; + adapter: BatchEnrichmentAdapter; + resultById: Map; + companyIdByMatchKeyCache: CompanyIdByMatchKeyCache; +}): Promise => { + let recordNodes: TNode[]; + try { + recordNodes = await adapter.readRecords({ client, recordIds }); + } catch (readError) { + const readErrorMessage = toErrorMessage(readError); + for (const recordId of recordIds) { + resultById.set( + recordId, + buildErrorResult({ recordId, error: readErrorMessage }), + ); + } + + return; + } + + const nodeByRecordId = new Map( + recordNodes.map((recordNode) => [adapter.getNodeId(recordNode), recordNode]), + ); + + const recordsToEnrich: { recordId: string; node: TNode; params: TParams }[] = + []; + for (const recordId of recordIds) { + const recordNode = nodeByRecordId.get(recordId); + if (!isDefined(recordNode)) { + resultById.set( + recordId, + buildErrorResult({ + recordId, + error: `${adapter.objectNameSingular} ${recordId} not found`, + }), + ); + continue; + } + + const matchParams = adapter.extractParams({ node: recordNode, input }); + if (!isDefined(matchParams)) { + resultById.set( + recordId, + buildSkippedResult({ + recordId, + message: adapter.noIdentifierMessage, + }), + ); + continue; + } + + recordsToEnrich.push({ recordId, node: recordNode, params: matchParams }); + } + + if (recordsToEnrich.length === 0) { + return; + } + + const enrichedAt = nowIso(); + const recordIdsToMarkAsError: string[] = []; + + let pdlEnrichmentOutcomes: PdlEnrichResult[]; + try { + pdlEnrichmentOutcomes = await adapter.enrichBatch( + recordsToEnrich.map((recordToEnrich) => recordToEnrich.params), + ); + } catch (enrichBatchError) { + const enrichBatchErrorMessage = toErrorMessage(enrichBatchError); + for (const recordToEnrich of recordsToEnrich) { + resultById.set( + recordToEnrich.recordId, + buildErrorResult({ + recordId: recordToEnrich.recordId, + error: enrichBatchErrorMessage, + }), + ); + } + await writeErrorStatusWithBackoff({ + adapter, + client, + recordIds: recordsToEnrich.map((recordToEnrich) => recordToEnrich.recordId), + enrichedAt, + }); + + return; + } + + const notFoundRecordIds: string[] = []; + const matchedRecordsToPersist: { + recordId: string; + data: Record; + }[] = []; + + for (let index = 0; index < recordsToEnrich.length; index++) { + const { recordId, node: recordNode } = recordsToEnrich[index]; + const enrichmentOutcome = pdlEnrichmentOutcomes[index]; + + if (!isDefined(enrichmentOutcome) || enrichmentOutcome.outcome === 'error') { + const enrichmentErrorMessage = isDefined(enrichmentOutcome) + ? enrichmentOutcome.message + : 'People Data Labs returned no response for this record.'; + resultById.set( + recordId, + buildErrorResult({ recordId, error: enrichmentErrorMessage }), + ); + recordIdsToMarkAsError.push(recordId); + continue; + } + + if (enrichmentOutcome.outcome === 'not_found') { + notFoundRecordIds.push(recordId); + continue; + } + + try { + const matchedRecordData = await adapter.buildMatchedData({ + client, + node: recordNode, + outcome: enrichmentOutcome, + enrichedAt, + companyIdByMatchKeyCache, + overrideExistingValues: input.overrideExistingValues === true, + }); + matchedRecordsToPersist.push({ recordId, data: matchedRecordData }); + } catch (buildMatchedDataError) { + resultById.set( + recordId, + buildErrorResult({ + recordId, + error: toErrorMessage(buildMatchedDataError), + }), + ); + recordIdsToMarkAsError.push(recordId); + } + } + + if (matchedRecordsToPersist.length > 0) { + const settledWriteResults = await Promise.allSettled( + matchedRecordsToPersist.map((matchedRecord) => + adapter.updateOne({ + client, + recordId: matchedRecord.recordId, + data: matchedRecord.data, + }), + ), + ); + + settledWriteResults.forEach((writeResult, index) => { + const matchedRecord = matchedRecordsToPersist[index]; + if (writeResult.status === 'fulfilled') { + resultById.set( + matchedRecord.recordId, + buildMatchedResult({ + recordId: matchedRecord.recordId, + updatedFields: Object.keys(matchedRecord.data).filter( + (fieldName) => !INTERNAL_BOOKKEEPING_FIELDS.has(fieldName), + ), + }), + ); + } else { + resultById.set( + matchedRecord.recordId, + buildErrorResult({ + recordId: matchedRecord.recordId, + error: toErrorMessage(writeResult.reason), + }), + ); + recordIdsToMarkAsError.push(matchedRecord.recordId); + } + }); + } + + if (notFoundRecordIds.length > 0) { + try { + await adapter.updateManyStatus({ + client, + recordIds: notFoundRecordIds, + data: { + pdlEnrichmentStatus: 'NOT_FOUND', + pdlLastEnrichedAt: enrichedAt, + }, + }); + for (const recordId of notFoundRecordIds) { + resultById.set(recordId, buildNotFoundResult(recordId)); + } + } catch (notFoundStatusWriteError) { + const notFoundStatusWriteErrorMessage = toErrorMessage( + notFoundStatusWriteError, + ); + for (const recordId of notFoundRecordIds) { + resultById.set( + recordId, + buildErrorResult({ + recordId, + error: notFoundStatusWriteErrorMessage, + }), + ); + recordIdsToMarkAsError.push(recordId); + } + } + } + + await writeErrorStatusWithBackoff({ + adapter, + client, + recordIds: recordIdsToMarkAsError, + enrichedAt, + }); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/enrich-companies.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/enrich-companies.ts new file mode 100644 index 0000000000000..ee225f42e6a5d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/enrich-companies.ts @@ -0,0 +1,21 @@ +import { postPdlBulkEnrich } from 'src/logic-functions/utils/post-pdl-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 enrichCompanies = ( + params: PdlCompanyEnrichParams[], +): Promise[]> => + postPdlBulkEnrich({ + path: '/company/enrich/bulk', + requests: params.map((entry) => + pruneUndefined({ + pdl_id: entry.pdlId, + website: entry.website, + profile: entry.profile, + name: entry.name, + min_likelihood: entry.minLikelihood, + }), + ), + }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/enrich-people.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/enrich-people.ts new file mode 100644 index 0000000000000..fd89776c6cba6 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/enrich-people.ts @@ -0,0 +1,22 @@ +import { postPdlBulkEnrich } from 'src/logic-functions/utils/post-pdl-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 enrichPeople = ( + params: PdlPersonEnrichParams[], +): Promise[]> => + postPdlBulkEnrich({ + path: '/person/bulk', + requests: params.map((entry) => + pruneUndefined({ + pdl_id: entry.pdlId, + profile: entry.profile, + email: entry.email, + name: entry.name, + company: entry.company, + min_likelihood: entry.minLikelihood, + }), + ), + }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-company-match-params.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-company-match-params.ts new file mode 100644 index 0000000000000..9598613609351 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-company-match-params.ts @@ -0,0 +1,52 @@ +import { normalizeDomain } from 'src/logic-functions/utils/normalize-domain'; +import { normalizeLinkedinUrl } from 'src/logic-functions/utils/normalize-linkedin-url'; +import { resolveMinLikelihood } from 'src/logic-functions/utils/resolve-min-likelihood'; +import { toText } from 'src/logic-functions/utils/to-text'; +import { type BulkEnrichInput } from 'src/types/bulk-enrich-input'; +import { type CompanyNode } from 'src/types/company-node'; +import { type PdlCompanyEnrichParams } from 'src/types/pdl-company-enrich-params'; +import { isDefined } from 'src/utils/is-defined'; +import { pruneUndefined } from 'src/utils/prune-undefined'; + +export const extractCompanyMatchParams = ({ + node, + input, +}: { + node: CompanyNode; + input: BulkEnrichInput; +}): PdlCompanyEnrichParams | undefined => { + const existingPdlId = toText(node.pdlId); + if (isDefined(existingPdlId)) { + return { + pdlId: existingPdlId, + minLikelihood: resolveMinLikelihood({ + inputMinLikelihood: input.minLikelihood, + hasStrongIdentifier: true, + }), + }; + } + + const websiteDomain = normalizeDomain(node.domainName?.primaryLinkUrl); + const linkedinProfileUrl = normalizeLinkedinUrl(node.linkedinLink?.primaryLinkUrl); + const companyName = toText(node.name); + const hasStrongIdentifier = + isDefined(websiteDomain) || isDefined(linkedinProfileUrl); + + const companyMatchParams = pruneUndefined({ + website: websiteDomain, + profile: linkedinProfileUrl, + name: companyName, + }); + + if (Object.keys(companyMatchParams).length === 0) { + return undefined; + } + + return { + ...companyMatchParams, + minLikelihood: resolveMinLikelihood({ + inputMinLikelihood: input.minLikelihood, + hasStrongIdentifier, + }), + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-pdl-error-message.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-pdl-error-message.ts new file mode 100644 index 0000000000000..1bf08c8de0c53 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-pdl-error-message.ts @@ -0,0 +1,47 @@ +import { isNonEmptyString, isObject, isString } from '@sniptt/guards'; + +import { isDefined } from 'src/utils/is-defined'; + +const extractMessageFromValue = ( + messageValue: unknown, +): string | undefined => { + if (isNonEmptyString(messageValue)) { + return messageValue; + } + + if (Array.isArray(messageValue)) { + const joinedMessages = messageValue.filter(isString).join('; '); + + return isNonEmptyString(joinedMessages) ? joinedMessages : undefined; + } + + return undefined; +}; + +export const extractPdlErrorMessage = ({ + json, + httpStatus, +}: { + json: Record; + httpStatus: number; +}): string => { + const errorField = json.error; + + if (isObject(errorField)) { + const messageFromErrorObject = extractMessageFromValue( + (errorField as Record).message, + ); + if (isDefined(messageFromErrorObject)) { + return messageFromErrorObject; + } + } + + const messageFromTopLevelField = + extractMessageFromValue(errorField) ?? + extractMessageFromValue(json.message); + if (isDefined(messageFromTopLevelField)) { + return messageFromTopLevelField; + } + + return `PDL request failed (HTTP ${httpStatus}).`; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-person-match-params.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-person-match-params.ts new file mode 100644 index 0000000000000..608a76cfb52d4 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-person-match-params.ts @@ -0,0 +1,59 @@ +import { buildPersonNameParam } from 'src/logic-functions/utils/build-person-name-param'; +import { resolveMinLikelihood } from 'src/logic-functions/utils/resolve-min-likelihood'; +import { toText } from 'src/logic-functions/utils/to-text'; +import { type BulkEnrichInput } from 'src/types/bulk-enrich-input'; +import { type PdlPersonEnrichParams } from 'src/types/pdl-person-enrich-params'; +import { type PersonNode } from 'src/types/person-node'; +import { isDefined } from 'src/utils/is-defined'; +import { pruneUndefined } from 'src/utils/prune-undefined'; + +export const extractPersonMatchParams = ({ + node, + input, +}: { + node: PersonNode; + input: BulkEnrichInput; +}): PdlPersonEnrichParams | undefined => { + const existingPdlId = toText(node.pdlId); + if (isDefined(existingPdlId)) { + return { + pdlId: existingPdlId, + minLikelihood: resolveMinLikelihood({ + inputMinLikelihood: input.minLikelihood, + hasStrongIdentifier: true, + }), + }; + } + + const linkedinProfileUrl = toText(node.linkedinLink?.primaryLinkUrl); + const primaryEmail = toText(node.emails?.primaryEmail); + const fullName = buildPersonNameParam({ + firstName: node.name?.firstName, + lastName: node.name?.lastName, + }); + const companyName = toText(node.company?.name); + const hasStrongIdentifier = + isDefined(linkedinProfileUrl) || isDefined(primaryEmail); + + const nameIsUsableAsMatchSignal = + isDefined(fullName) && (hasStrongIdentifier || isDefined(companyName)); + + const personMatchParams = pruneUndefined({ + profile: linkedinProfileUrl, + email: primaryEmail, + name: nameIsUsableAsMatchSignal ? fullName : undefined, + company: nameIsUsableAsMatchSignal ? companyName : undefined, + }); + + if (Object.keys(personMatchParams).length === 0) { + return undefined; + } + + return { + ...personMatchParams, + minLikelihood: resolveMinLikelihood({ + inputMinLikelihood: input.minLikelihood, + hasStrongIdentifier, + }), + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-record-ids.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-record-ids.ts new file mode 100644 index 0000000000000..2db9aa4b5a236 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/extract-record-ids.ts @@ -0,0 +1,18 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { type RecordInput } from 'src/types/bulk-enrich-input'; + +export const extractRecordIds = ( + records: RecordInput | RecordInput[], +): string[] => { + const recordsAsArray = + records === null || records === undefined + ? [] + : Array.isArray(records) + ? records + : [records]; + + return recordsAsArray + .map((record) => (typeof record === 'string' ? record : record?.id)) + .filter((id): id is string => isNonEmptyString(id)); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-company-id-by-filter.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-company-id-by-filter.ts new file mode 100644 index 0000000000000..540b56c57f630 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-company-id-by-filter.ts @@ -0,0 +1,28 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +type CompaniesConnection = { edges?: { node: { id: string } }[] }; + +export const findCompanyIdByFilter = async ({ + client, + filter, + requireUnique = false, +}: { + client: CoreApiClient; + filter: Record; + requireUnique?: boolean; +}): Promise => { + const result = (await client.query({ + companies: { + __args: { filter, first: requireUnique ? 2 : 1 }, + edges: { node: { id: true } }, + }, + })) as { companies?: CompaniesConnection }; + + const edges = result.companies?.edges ?? []; + + if (requireUnique && edges.length !== 1) { + return undefined; + } + + return edges[0]?.node?.id; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-company-id.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-company-id.ts new file mode 100644 index 0000000000000..266c59cde1ded --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-company-id.ts @@ -0,0 +1,59 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { findCompanyIdByFilter } from 'src/logic-functions/utils/find-company-id-by-filter'; +import { type CompanyMatchKeys } from 'src/types/company-match-keys'; +import { isDefined } from 'src/utils/is-defined'; + +export const findCompanyId = async ({ + client, + matchKeys, +}: { + client: CoreApiClient; + matchKeys: CompanyMatchKeys; +}): Promise => { + const { pdlId, website, linkedinUrl, name } = matchKeys; + + if (isNonEmptyString(pdlId)) { + const matchById = await findCompanyIdByFilter({ + client, + filter: { pdlId: { eq: pdlId } }, + }); + if (isDefined(matchById)) { + return matchById; + } + } + + if (isNonEmptyString(website)) { + const matchByDomain = await findCompanyIdByFilter({ + client, + filter: { domainName: { primaryLinkUrl: { eq: website } } }, + }); + if (isDefined(matchByDomain)) { + return matchByDomain; + } + } + + if (isNonEmptyString(linkedinUrl)) { + const matchByLinkedin = await findCompanyIdByFilter({ + client, + filter: { linkedinLink: { primaryLinkUrl: { eq: linkedinUrl } } }, + }); + if (isDefined(matchByLinkedin)) { + return matchByLinkedin; + } + } + + if (isNonEmptyString(name)) { + const matchByName = await findCompanyIdByFilter({ + client, + filter: { name: { eq: name } }, + requireUnique: true, + }); + if (isDefined(matchByName)) { + return matchByName; + } + } + + return undefined; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-existing-workflow-id.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-existing-workflow-id.ts new file mode 100644 index 0000000000000..43e333751f627 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-existing-workflow-id.ts @@ -0,0 +1,18 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +export const findExistingWorkflowId = async ({ + client, + name, +}: { + client: CoreApiClient; + name: string; +}): Promise => { + const result = (await client.query({ + workflows: { + __args: { filter: { name: { eq: name } } }, + edges: { node: { id: true } }, + }, + })) as { workflows?: { edges?: { node?: { id?: string } }[] } }; + + return result.workflows?.edges?.[0]?.node?.id; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-or-create-current-company.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-or-create-current-company.ts new file mode 100644 index 0000000000000..ad7c7616a7aa3 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/find-or-create-current-company.ts @@ -0,0 +1,108 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { PdlOperationError } from 'src/logic-functions/errors/pdl-operation-error'; +import { buildCompanyCreateData } from 'src/logic-functions/utils/build-company-create-data'; +import { buildCompanyMatchKeys } from 'src/logic-functions/utils/build-company-match-keys'; +import { findCompanyId } from 'src/logic-functions/utils/find-company-id'; +import { type CompanyIdByMatchKeyCache } from 'src/types/company-id-by-match-key-cache'; +import { type CompanyMatchKeys } from 'src/types/company-match-keys'; +import { type PdlPersonData } from 'src/types/pdl-person-data'; +import { isDefined } from 'src/utils/is-defined'; +import { isUniqueViolationError } from 'src/utils/is-unique-violation-error'; + +type CreateCompanyResult = { createCompany?: { id?: string } }; + +const findOrCreateUncachedCompany = async ({ + client, + personData, + companyMatchKeys, +}: { + client: CoreApiClient; + personData: PdlPersonData; + companyMatchKeys: CompanyMatchKeys; +}): Promise => { + const existingCompanyId = await findCompanyId({ + client, + matchKeys: companyMatchKeys, + }); + if (isDefined(existingCompanyId)) { + return existingCompanyId; + } + + const canCreateNewCompany = + isNonEmptyString(companyMatchKeys.name) || + isNonEmptyString(companyMatchKeys.website); + + if (!canCreateNewCompany) { + return undefined; + } + + try { + const createCompanyResult = (await client.mutation({ + createCompany: { + __args: { data: buildCompanyCreateData(personData) }, + id: true, + }, + })) as CreateCompanyResult; + + const createdCompanyId = createCompanyResult.createCompany?.id; + + if (!isDefined(createdCompanyId)) { + throw new PdlOperationError('Failed to create company: no id returned.'); + } + + return createdCompanyId; + } catch (createCompanyError) { + if (!isUniqueViolationError(createCompanyError)) { + throw createCompanyError; + } + + const raceWinnerCompanyId = await findCompanyId({ + client, + matchKeys: companyMatchKeys, + }); + if (isDefined(raceWinnerCompanyId)) { + return raceWinnerCompanyId; + } + + throw createCompanyError; + } +}; + +export const findOrCreateCurrentCompany = async ({ + client, + personData, + companyIdByMatchKeyCache, +}: { + client: CoreApiClient; + personData: PdlPersonData; + companyIdByMatchKeyCache: CompanyIdByMatchKeyCache; +}): Promise => { + const companyMatchKeys = buildCompanyMatchKeys(personData); + + const hasAnyCompanyMatchKey = + isNonEmptyString(companyMatchKeys.pdlId) || + isNonEmptyString(companyMatchKeys.website) || + isNonEmptyString(companyMatchKeys.linkedinUrl) || + isNonEmptyString(companyMatchKeys.name); + + if (!hasAnyCompanyMatchKey) { + return undefined; + } + + const companyMatchKeyCacheKey = JSON.stringify(companyMatchKeys); + if (companyIdByMatchKeyCache.has(companyMatchKeyCacheKey)) { + return companyIdByMatchKeyCache.get(companyMatchKeyCacheKey); + } + + const resolvedCompanyId = await findOrCreateUncachedCompany({ + client, + personData, + companyMatchKeys, + }); + + companyIdByMatchKeyCache.set(companyMatchKeyCacheKey, resolvedCompanyId); + + return resolvedCompanyId; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/get-pdl-api-key.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/get-pdl-api-key.ts new file mode 100644 index 0000000000000..887afabf36a9b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/get-pdl-api-key.ts @@ -0,0 +1,15 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { PdlConfigError } from 'src/logic-functions/errors/pdl-config-error'; + +export const getPdlApiKey = (): string => { + const apiKey = process.env.PDL_API_KEY?.trim(); + + if (!isNonEmptyString(apiKey)) { + throw new PdlConfigError( + 'PDL_API_KEY is not set. The workspace admin must configure the People Data Labs API key in Settings -> Apps.', + ); + } + + return apiKey; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/internal-field-names.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/internal-field-names.ts new file mode 100644 index 0000000000000..642fad9a1bad9 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/internal-field-names.ts @@ -0,0 +1,6 @@ +export const INTERNAL_BOOKKEEPING_FIELDS: ReadonlySet = new Set([ + 'pdlRawPayload', + 'pdlLastEnrichedAt', + 'pdlEnrichmentStatus', + 'pdlLikelihood', +]); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-address.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-address.ts new file mode 100644 index 0000000000000..42a935c6095ce --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-address.ts @@ -0,0 +1,20 @@ +import { isObject } from '@sniptt/guards'; + +import { isEmptyText } from 'src/logic-functions/utils/is-empty-text'; + +export const isEmptyAddress = (value: unknown): boolean => { + if (!isObject(value)) { + return true; + } + + const record = value as Record; + + return ( + isEmptyText(record.addressStreet1) && + isEmptyText(record.addressStreet2) && + isEmptyText(record.addressCity) && + isEmptyText(record.addressPostcode) && + isEmptyText(record.addressState) && + isEmptyText(record.addressCountry) + ); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-emails.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-emails.ts new file mode 100644 index 0000000000000..763069e1fbd2a --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-emails.ts @@ -0,0 +1,13 @@ +import { isObject } from '@sniptt/guards'; + +import { isEmptyText } from 'src/logic-functions/utils/is-empty-text'; + +export const isEmptyEmails = (value: unknown): boolean => { + if (!isObject(value)) { + return true; + } + + const record = value as Record; + + return isEmptyText(record.primaryEmail); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-full-name.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-full-name.ts new file mode 100644 index 0000000000000..c37e23cb75d51 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-full-name.ts @@ -0,0 +1,13 @@ +import { isObject } from '@sniptt/guards'; + +import { isEmptyText } from 'src/logic-functions/utils/is-empty-text'; + +export const isEmptyFullName = (value: unknown): boolean => { + if (!isObject(value)) { + return true; + } + + const record = value as Record; + + return isEmptyText(record.firstName) && isEmptyText(record.lastName); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-links.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-links.ts new file mode 100644 index 0000000000000..ffbbe340e2051 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-links.ts @@ -0,0 +1,13 @@ +import { isObject } from '@sniptt/guards'; + +import { isEmptyText } from 'src/logic-functions/utils/is-empty-text'; + +export const isEmptyLinks = (value: unknown): boolean => { + if (!isObject(value)) { + return true; + } + + const record = value as Record; + + return isEmptyText(record.primaryLinkUrl); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-phones.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-phones.ts new file mode 100644 index 0000000000000..f0355f91f4550 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-phones.ts @@ -0,0 +1,13 @@ +import { isObject } from '@sniptt/guards'; + +import { isEmptyText } from 'src/logic-functions/utils/is-empty-text'; + +export const isEmptyPhones = (value: unknown): boolean => { + if (!isObject(value)) { + return true; + } + + const record = value as Record; + + return isEmptyText(record.primaryPhoneNumber); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-text.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-text.ts new file mode 100644 index 0000000000000..feadf2681959b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/is-empty-text.ts @@ -0,0 +1,4 @@ +import { toText } from 'src/logic-functions/utils/to-text'; +import { isDefined } from 'src/utils/is-defined'; + +export const isEmptyText = (value: unknown): boolean => !isDefined(toText(value)); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/map-company.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/map-company.ts new file mode 100644 index 0000000000000..96dad75caa6b9 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/map-company.ts @@ -0,0 +1,115 @@ +import { COMPANY_TYPE_OPTIONS } from 'src/constants/company-type-options'; +import { FUNDING_STAGE_OPTIONS } from 'src/constants/funding-stage-options'; +import { INDUSTRY_OPTIONS } from 'src/constants/industry-options'; +import { LOCATION_CONTINENT_OPTIONS } from 'src/constants/location-continent-options'; +import { METRO_OPTIONS } from 'src/constants/metro-options'; +import { MIC_EXCHANGE_OPTIONS } from 'src/constants/mic-exchange-options'; +import { SIZE_OPTIONS } from 'src/constants/size-options'; +import { toJsonArray } from 'src/logic-functions/utils/to-json-array'; +import { toJsonObject } from 'src/logic-functions/utils/to-json-object'; +import { toNumber } from 'src/logic-functions/utils/to-number'; +import { toStringArray } from 'src/logic-functions/utils/to-string-array'; +import { toText } from 'src/logic-functions/utils/to-text'; +import { buildAddress } from 'src/logic-functions/utils/build-address'; +import { buildCurrencyFromUsd } from 'src/logic-functions/utils/build-currency-from-usd'; +import { buildLinks } from 'src/logic-functions/utils/build-links'; +import { normalizeDomain } from 'src/logic-functions/utils/normalize-domain'; +import { normalizeLinkedinUrl } from 'src/logic-functions/utils/normalize-linkedin-url'; +import { parsePartialDate } from 'src/logic-functions/utils/parse-partial-date'; +import { buildAllowedValues } from 'src/logic-functions/utils/build-allowed-values'; +import { pickMultiSelect } from 'src/logic-functions/utils/pick-multi-select'; +import { pickSelect } from 'src/logic-functions/utils/pick-select'; +import { sizeTransform } from 'src/logic-functions/utils/size-transform'; +import { type MappedRecord } from 'src/types/mapped-record'; +import { type PdlCompanyData } from 'src/types/pdl-company-data'; +import { pruneUndefined } from 'src/utils/prune-undefined'; + +const INDUSTRY_VALUES = buildAllowedValues(INDUSTRY_OPTIONS); +const COMPANY_TYPE_VALUES = buildAllowedValues(COMPANY_TYPE_OPTIONS); +const SIZE_VALUES = buildAllowedValues(SIZE_OPTIONS); +const METRO_VALUES = buildAllowedValues(METRO_OPTIONS); +const LOCATION_CONTINENT_VALUES = buildAllowedValues(LOCATION_CONTINENT_OPTIONS); +const MIC_EXCHANGE_VALUES = buildAllowedValues(MIC_EXCHANGE_OPTIONS); +const FUNDING_STAGE_VALUES = buildAllowedValues(FUNDING_STAGE_OPTIONS); + +export const mapCompany = (companyData: PdlCompanyData): MappedRecord => { + const location = companyData.location ?? {}; + + const standard = pruneUndefined({ + name: toText(companyData.display_name) ?? toText(companyData.name), + domainName: buildLinks({ url: normalizeDomain(companyData.website) }), + linkedinLink: buildLinks({ url: normalizeLinkedinUrl(companyData.linkedin_url) }), + address: buildAddress({ + street1: location.street_address, + street2: location.address_line_2, + city: location.locality, + postcode: location.postal_code, + state: location.region, + country: location.country, + geo: location.geo, + }), + }); + + const pdl = pruneUndefined({ + pdlId: toText(companyData.id), + + pdlIndustry: pickSelect({ raw: companyData.industry, allowedValues: INDUSTRY_VALUES }), + pdlIndustryDetail: toText(companyData.industry_v2), + pdlCompanyType: pickSelect({ + raw: companyData.type, + allowedValues: COMPANY_TYPE_VALUES, + }), + pdlSizeRange: pickSelect({ + raw: companyData.size, + allowedValues: SIZE_VALUES, + transform: sizeTransform, + }), + pdlLocationMetro: pickSelect({ + raw: location.metro, + allowedValues: METRO_VALUES, + }), + pdlLocationContinent: pickSelect({ + raw: location.continent, + allowedValues: LOCATION_CONTINENT_VALUES, + }), + pdlMicExchange: pickSelect({ + raw: companyData.mic_exchange, + allowedValues: MIC_EXCHANGE_VALUES, + }), + + pdlLegalName: toText(companyData.legal_name), + pdlHeadline: toText(companyData.headline), + pdlSummary: toText(companyData.summary), + pdlTicker: toText(companyData.ticker), + pdlLinkedinId: toText(companyData.linkedin_id), + + pdlEmployeeCount: toNumber(companyData.employee_count), + pdlFoundedYear: toNumber(companyData.founded), + pdlNumberFundingRounds: toNumber(companyData.number_funding_rounds), + + pdlTotalFunding: buildCurrencyFromUsd(companyData.total_funding_raised), + pdlLatestFundingStage: pickSelect({ + raw: companyData.latest_funding_stage, + allowedValues: FUNDING_STAGE_VALUES, + }), + pdlFundingStages: pickMultiSelect({ + rawValues: companyData.funding_stages, + allowedValues: FUNDING_STAGE_VALUES, + }), + pdlLastFundingDate: parsePartialDate(companyData.last_funding_date), + + pdlTwitterUrl: buildLinks({ url: companyData.twitter_url }), + pdlFacebookUrl: buildLinks({ url: companyData.facebook_url }), + + pdlTags: toStringArray(companyData.tags), + pdlAlternativeNames: toStringArray(companyData.alternative_names), + pdlAlternativeDomains: toStringArray(companyData.alternative_domains), + pdlAffiliatedProfiles: toStringArray(companyData.profiles), + + pdlNaics: toJsonArray(companyData.naics), + pdlSic: toJsonArray(companyData.sic), + pdlEmployeeCountByCountry: toJsonObject(companyData.employee_count_by_country), + }); + + return { standard, pdl }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/map-person.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/map-person.ts new file mode 100644 index 0000000000000..cce3651f2abd6 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/map-person.ts @@ -0,0 +1,136 @@ +import { isArray, isString } from '@sniptt/guards'; + +import { INDUSTRY_OPTIONS } from 'src/constants/industry-options'; +import { INFERRED_SALARY_OPTIONS } from 'src/constants/inferred-salary-options'; +import { JOB_ROLE_OPTIONS } from 'src/constants/job-role-options'; +import { JOB_TITLE_CLASS_OPTIONS } from 'src/constants/job-title-class-options'; +import { JOB_TITLE_SUB_ROLE_OPTIONS } from 'src/constants/job-title-sub-role-options'; +import { METRO_OPTIONS } from 'src/constants/metro-options'; +import { SENIORITY_OPTIONS } from 'src/constants/seniority-options'; +import { SEX_OPTIONS } from 'src/constants/sex-options'; +import { toJsonArray } from 'src/logic-functions/utils/to-json-array'; +import { toNumber } from 'src/logic-functions/utils/to-number'; +import { toStringArray } from 'src/logic-functions/utils/to-string-array'; +import { toText } from 'src/logic-functions/utils/to-text'; +import { buildAddress } from 'src/logic-functions/utils/build-address'; +import { buildEmails } from 'src/logic-functions/utils/build-emails'; +import { buildFullName } from 'src/logic-functions/utils/build-full-name'; +import { buildLinks } from 'src/logic-functions/utils/build-links'; +import { buildPhones } from 'src/logic-functions/utils/build-phones'; +import { parsePartialDate } from 'src/logic-functions/utils/parse-partial-date'; +import { buildAllowedValues } from 'src/logic-functions/utils/build-allowed-values'; +import { pickMultiSelect } from 'src/logic-functions/utils/pick-multi-select'; +import { pickSelect } from 'src/logic-functions/utils/pick-select'; +import { salaryTransform } from 'src/logic-functions/utils/salary-transform'; +import { type MappedRecord } from 'src/types/mapped-record'; +import { type PdlPersonData } from 'src/types/pdl-person-data'; +import { pruneUndefined } from 'src/utils/prune-undefined'; + +const SENIORITY_VALUES = buildAllowedValues(SENIORITY_OPTIONS); +const JOB_ROLE_VALUES = buildAllowedValues(JOB_ROLE_OPTIONS); +const JOB_TITLE_CLASS_VALUES = buildAllowedValues(JOB_TITLE_CLASS_OPTIONS); +const JOB_TITLE_SUB_ROLE_VALUES = buildAllowedValues(JOB_TITLE_SUB_ROLE_OPTIONS); +const INDUSTRY_VALUES = buildAllowedValues(INDUSTRY_OPTIONS); +const INFERRED_SALARY_VALUES = buildAllowedValues(INFERRED_SALARY_OPTIONS); +const SEX_VALUES = buildAllowedValues(SEX_OPTIONS); +const METRO_VALUES = buildAllowedValues(METRO_OPTIONS); + +export const mapPerson = (personData: PdlPersonData): MappedRecord => { + const emailEntries = (isArray(personData.emails) ? personData.emails : []).map( + (rawEmail) => (isString(rawEmail) ? rawEmail : rawEmail.address), + ); + + const standard = pruneUndefined({ + name: buildFullName({ + firstName: personData.first_name, + lastName: personData.last_name, + fullName: personData.full_name, + }), + emails: buildEmails([ + personData.work_email, + personData.recommended_personal_email, + ...(isArray(personData.personal_emails) ? personData.personal_emails : []), + ...emailEntries, + ]), + phones: buildPhones([ + personData.mobile_phone, + ...(isArray(personData.phone_numbers) ? personData.phone_numbers : []), + ]), + jobTitle: toText(personData.job_title), + linkedinLink: buildLinks({ url: personData.linkedin_url }), + }); + + const pdl = pruneUndefined({ + pdlId: toText(personData.id), + + pdlSeniority: pickMultiSelect({ + rawValues: personData.job_title_levels, + allowedValues: SENIORITY_VALUES, + }), + pdlJobRole: pickSelect({ + raw: personData.job_title_role, + allowedValues: JOB_ROLE_VALUES, + }), + pdlJobTitleClass: pickSelect({ + raw: personData.job_title_class, + allowedValues: JOB_TITLE_CLASS_VALUES, + }), + pdlJobTitleSubRole: pickSelect({ + raw: personData.job_title_sub_role, + allowedValues: JOB_TITLE_SUB_ROLE_VALUES, + }), + pdlIndustry: pickSelect({ + raw: personData.industry, + allowedValues: INDUSTRY_VALUES, + }), + pdlInferredSalary: pickSelect({ + raw: personData.inferred_salary, + allowedValues: INFERRED_SALARY_VALUES, + transform: salaryTransform, + }), + pdlSex: pickSelect({ raw: personData.sex, allowedValues: SEX_VALUES }), + pdlLocationMetro: pickSelect({ + raw: personData.location_metro, + allowedValues: METRO_VALUES, + }), + + pdlHeadline: toText(personData.headline), + pdlSummary: toText(personData.summary), + pdlJobSummary: toText(personData.job_summary), + pdlJobOnetCode: toText(personData.job_onet_code), + pdlLinkedinUsername: toText(personData.linkedin_username), + + pdlYearsExperience: toNumber(personData.inferred_years_experience), + pdlLinkedinConnections: toNumber(personData.linkedin_connections), + pdlBirthYear: toNumber(personData.birth_year), + + pdlBirthDate: parsePartialDate(personData.birth_date), + pdlJobStartDate: parsePartialDate(personData.job_start_date), + + pdlGithubUrl: buildLinks({ url: personData.github_url }), + pdlTwitterUrl: buildLinks({ url: personData.twitter_url }), + pdlFacebookUrl: buildLinks({ url: personData.facebook_url }), + + pdlSkills: toStringArray(personData.skills), + pdlInterests: toStringArray(personData.interests), + pdlNameAliases: toStringArray(personData.name_aliases), + + pdlExperience: toJsonArray(personData.experience), + pdlEducation: toJsonArray(personData.education), + pdlCertifications: toJsonArray(personData.certifications), + pdlProfiles: toJsonArray(personData.profiles), + pdlLanguages: toJsonArray(personData.languages), + + pdlLocation: buildAddress({ + street1: personData.location_street_address, + street2: personData.location_address_line_2, + city: personData.location_locality, + postcode: personData.location_postal_code, + state: personData.location_region, + country: personData.location_country, + geo: personData.location_geo, + }), + }); + + return { standard, pdl }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/normalize-domain.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/normalize-domain.ts new file mode 100644 index 0000000000000..b415f620d8692 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/normalize-domain.ts @@ -0,0 +1,22 @@ +import { toText } from 'src/logic-functions/utils/to-text'; +import { isDefined } from 'src/utils/is-defined'; + +const SCHEME_REGEX = /^[a-z][a-z0-9+.-]*:\/\//; +const LEADING_WWW_REGEX = /^www\./; + +export const normalizeDomain = ( + rawDomainValue: unknown, +): string | undefined => { + const domainText = toText(rawDomainValue); + if (!isDefined(domainText)) { + return undefined; + } + + const bareHost = domainText + .toLowerCase() + .replace(SCHEME_REGEX, '') + .split('/')[0] + .replace(LEADING_WWW_REGEX, ''); + + return bareHost === '' ? undefined : bareHost; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/normalize-linkedin-url.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/normalize-linkedin-url.ts new file mode 100644 index 0000000000000..2cf6930a492bb --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/normalize-linkedin-url.ts @@ -0,0 +1,23 @@ +import { toText } from 'src/logic-functions/utils/to-text'; +import { isDefined } from 'src/utils/is-defined'; + +const SCHEME_REGEX = /^[a-z][a-z0-9+.-]*:\/\//; +const LEADING_WWW_REGEX = /^www\./; +const TRAILING_SLASHES_REGEX = /\/+$/; + +export const normalizeLinkedinUrl = ( + rawLinkedinUrl: unknown, +): string | undefined => { + const linkedinUrlText = toText(rawLinkedinUrl); + if (!isDefined(linkedinUrlText)) { + return undefined; + } + + const canonicalUrlWithPath = linkedinUrlText + .toLowerCase() + .replace(SCHEME_REGEX, '') + .replace(LEADING_WWW_REGEX, '') + .replace(TRAILING_SLASHES_REGEX, ''); + + return canonicalUrlWithPath === '' ? undefined : canonicalUrlWithPath; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/now-iso.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/now-iso.ts new file mode 100644 index 0000000000000..15e92ff6b91bb --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/now-iso.ts @@ -0,0 +1 @@ +export const nowIso = (): string => new Date().toISOString(); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/parse-geo.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/parse-geo.ts new file mode 100644 index 0000000000000..5860a4ccca241 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/parse-geo.ts @@ -0,0 +1,33 @@ +import { toText } from 'src/logic-functions/utils/to-text'; +import { isDefined } from 'src/utils/is-defined'; + +const NULL_COORDINATES = { lat: null, lng: null } as const; + +export const parseGeo = ( + geo: unknown, +): { lat: number | null; lng: number | null } => { + const geoText = toText(geo); + + if (!isDefined(geoText)) { + return { ...NULL_COORDINATES }; + } + + const latitudeAndLongitudeParts = geoText.split(','); + if (latitudeAndLongitudeParts.length !== 2) { + return { ...NULL_COORDINATES }; + } + + const latitude = Number.parseFloat(latitudeAndLongitudeParts[0]); + const longitude = Number.parseFloat(latitudeAndLongitudeParts[1]); + + const isLatitudeInRange = + Number.isFinite(latitude) && latitude >= -90 && latitude <= 90; + const isLongitudeInRange = + Number.isFinite(longitude) && longitude >= -180 && longitude <= 180; + + if (!isLatitudeInRange || !isLongitudeInRange) { + return { ...NULL_COORDINATES }; + } + + return { lat: latitude, lng: longitude }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/parse-partial-date.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/parse-partial-date.ts new file mode 100644 index 0000000000000..64ee1ee1349fa --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/parse-partial-date.ts @@ -0,0 +1,33 @@ +import { toText } from 'src/logic-functions/utils/to-text'; +import { isDefined } from 'src/utils/is-defined'; + +const PARTIAL_DATE_REGEX = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/; + +export const parsePartialDate = (raw: unknown): string | undefined => { + const dateText = toText(raw); + + if (!isDefined(dateText)) { + return undefined; + } + + const partialDateMatch = dateText.match(PARTIAL_DATE_REGEX); + if (!isDefined(partialDateMatch)) { + return undefined; + } + + const [, year, month = '01', day = '01'] = partialDateMatch; + const monthNumber = Number(month); + const dayNumber = Number(day); + + const candidateDate = new Date(`${year}-${month}-${day}T00:00:00Z`); + const isRealCalendarDate = + !Number.isNaN(candidateDate.getTime()) && + candidateDate.getUTCMonth() + 1 === monthNumber && + candidateDate.getUTCDate() === dayNumber; + + if (!isRealCalendarDate) { + return undefined; + } + + return `${year}-${month}-${day}`; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/parse-pdl-item.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/parse-pdl-item.ts new file mode 100644 index 0000000000000..20d255c040f33 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/parse-pdl-item.ts @@ -0,0 +1,82 @@ +import { isNumber, isObject } from '@sniptt/guards'; + +import { extractPdlErrorMessage } from 'src/logic-functions/utils/extract-pdl-error-message'; +import { type PdlEnrichResult } from 'src/types/pdl-enrich-result'; +import { isDefined } from 'src/utils/is-defined'; + +const ASSUMED_SUCCESS_STATUS_WHEN_MISSING = 200; + +const ENVELOPE_FIELD_NAMES = new Set(['status', 'likelihood']); + +const extractMatchedData = ( + responseItem: Record, +): Record => { + if (isObject(responseItem.data)) { + return responseItem.data as Record; + } + + return Object.fromEntries( + Object.entries(responseItem).filter( + ([fieldName]) => !ENVELOPE_FIELD_NAMES.has(fieldName), + ), + ); +}; + +export const parsePdlItem = ({ + item, + requestedMinLikelihood, +}: { + item: unknown; + requestedMinLikelihood?: number; +}): PdlEnrichResult => { + if (!isObject(item)) { + return { + outcome: 'error', + httpStatus: 0, + message: 'People Data Labs returned a malformed response item.', + }; + } + + const responseItem = item as Record; + const httpStatus = isNumber(responseItem.status) + ? responseItem.status + : ASSUMED_SUCCESS_STATUS_WHEN_MISSING; + + if (httpStatus === 404) { + return { outcome: 'not_found', httpStatus: 404 }; + } + + if (httpStatus < 200 || httpStatus >= 300) { + return { + outcome: 'error', + httpStatus, + message: extractPdlErrorMessage({ json: responseItem, httpStatus }), + }; + } + + const matchedData = extractMatchedData(responseItem); + + if (Object.keys(matchedData).length === 0) { + return { outcome: 'not_found', httpStatus }; + } + + const matchLikelihood = isNumber(responseItem.likelihood) + ? responseItem.likelihood + : undefined; + + const isMatchBelowRequestedThreshold = + isDefined(requestedMinLikelihood) && + isDefined(matchLikelihood) && + matchLikelihood < requestedMinLikelihood; + + if (isMatchBelowRequestedThreshold) { + return { outcome: 'not_found', httpStatus }; + } + + return { + outcome: 'matched', + httpStatus, + likelihood: matchLikelihood, + data: matchedData as TData, + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/pick-multi-select.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/pick-multi-select.ts new file mode 100644 index 0000000000000..1759394c824fa --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/pick-multi-select.ts @@ -0,0 +1,32 @@ +import { isArray, isNonEmptyArray } from '@sniptt/guards'; + +import { pickSelect } from 'src/logic-functions/utils/pick-select'; +import { isDefined } from 'src/utils/is-defined'; +import { normalizeEnumValue } from 'src/utils/normalize-enum-value'; + +export const pickMultiSelect = ({ + rawValues, + allowedValues, + transform = normalizeEnumValue, +}: { + rawValues: unknown; + allowedValues: Set; + transform?: (value: string) => string | undefined; +}): string[] | undefined => { + if (!isArray(rawValues)) { + return undefined; + } + + const values: string[] = []; + const seenValues = new Set(); + + for (const raw of rawValues) { + const value = pickSelect({ raw, allowedValues, transform }); + if (isDefined(value) && !seenValues.has(value)) { + seenValues.add(value); + values.push(value); + } + } + + return isNonEmptyArray(values) ? values : undefined; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/pick-select.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/pick-select.ts new file mode 100644 index 0000000000000..98817cd7f38d5 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/pick-select.ts @@ -0,0 +1,30 @@ +import { isNonEmptyString, isString } from '@sniptt/guards'; + +import { isDefined } from 'src/utils/is-defined'; +import { normalizeEnumValue } from 'src/utils/normalize-enum-value'; + +export const pickSelect = ({ + raw, + allowedValues, + transform = normalizeEnumValue, +}: { + raw: unknown; + allowedValues: Set; + transform?: (value: string) => string | undefined; +}): string | undefined => { + if (!isString(raw)) { + return undefined; + } + + const trimmed = raw.trim(); + if (!isNonEmptyString(trimmed)) { + return undefined; + } + + const candidate = transform(trimmed); + if (!isDefined(candidate) || !allowedValues.has(candidate)) { + return undefined; + } + + return candidate; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/pick-writable-standard.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/pick-writable-standard.ts new file mode 100644 index 0000000000000..9797645c29d39 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/pick-writable-standard.ts @@ -0,0 +1,27 @@ +import { isDefined } from 'src/utils/is-defined'; + +export const pickWritableStandard = ({ + standard, + current, + emptyChecks, + overrideExistingValues, +}: { + standard: Record; + current: Record; + emptyChecks: Record boolean>; + overrideExistingValues?: boolean; +}): Record => { + const writableFields: Record = {}; + + for (const [fieldName, fieldValue] of Object.entries(standard)) { + const isEmptyCheck = emptyChecks[fieldName]; + if ( + isDefined(isEmptyCheck) && + (overrideExistingValues === true || isEmptyCheck(current[fieldName])) + ) { + writableFields[fieldName] = fieldValue; + } + } + + return writableFields; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/post-pdl-enrich.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/post-pdl-enrich.ts new file mode 100644 index 0000000000000..eff1e9a81958c --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/post-pdl-enrich.ts @@ -0,0 +1,90 @@ +import { isNumber, isObject } from '@sniptt/guards'; + +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 ({ + path, + requests, +}: { + path: string; + requests: Record[]; +}): Promise[]> => { + if (requests.length === 0) { + return []; + } + + const apiKey = getPdlApiKey(); + + const failAll = ({ + message, + httpStatus, + }: { + message: string; + httpStatus: number; + }): PdlEnrichResult[] => + requests.map(() => ({ outcome: 'error', httpStatus, message })); + + 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({ requests: requests.map((params) => ({ params })) }), + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + return failAll({ message: `PDL request failed: ${message}`, httpStatus: 0 }); + } + + let json: unknown; + try { + json = await response.json(); + } catch { + return failAll({ + message: `PDL returned a non-JSON response (HTTP ${response.status}).`, + httpStatus: response.status, + }); + } + + if (!response.ok) { + const message = isObject(json) + ? extractPdlErrorMessage({ + json: json as Record, + httpStatus: response.status, + }) + : `PDL request failed (HTTP ${response.status}).`; + + return failAll({ message, httpStatus: response.status }); + } + + const responseItems = Array.isArray(json) + ? json + : isObject(json) && Array.isArray((json as Record).responses) + ? ((json as Record).responses as unknown[]) + : []; + + if (responseItems.length !== requests.length) { + return failAll({ + message: `People Data Labs returned ${responseItems.length} results for ${requests.length} requests (HTTP ${response.status}).`, + httpStatus: response.status, + }); + } + + return requests.map((requestParams, index) => + parsePdlItem({ + item: responseItems[index], + requestedMinLikelihood: isNumber(requestParams.min_likelihood) + ? requestParams.min_likelihood + : undefined, + }), + ); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/read-companies.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/read-companies.ts new file mode 100644 index 0000000000000..1968dcfe54d10 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/read-companies.ts @@ -0,0 +1,53 @@ +import { isNonEmptyString, isObject } from '@sniptt/guards'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { type CompanyNode } from 'src/types/company-node'; + +export const readCompanies = async ({ + client, + recordIds, +}: { + client: CoreApiClient; + recordIds: string[]; +}): Promise => { + if (recordIds.length === 0) { + return []; + } + + const result = (await client.query({ + companies: { + __args: { filter: { id: { in: recordIds } }, first: recordIds.length }, + edges: { + node: { + id: true, + name: true, + domainName: { primaryLinkUrl: true }, + linkedinLink: { primaryLinkUrl: true }, + address: { + addressStreet1: true, + addressStreet2: true, + addressCity: true, + addressPostcode: true, + addressState: true, + addressCountry: true, + }, + pdlId: true, + pdlLastEnrichedAt: true, + }, + }, + }, + })) as { companies?: { edges?: { node: CompanyNode }[] } }; + + const edges = result.companies?.edges; + if (!Array.isArray(edges)) { + return []; + } + + return edges + .map((edge) => edge?.node) + .filter( + (companyNode): companyNode is CompanyNode => + isObject(companyNode) && + isNonEmptyString((companyNode as { id?: unknown }).id), + ); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/read-people.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/read-people.ts new file mode 100644 index 0000000000000..5a2be884fc171 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/read-people.ts @@ -0,0 +1,48 @@ +import { isNonEmptyString, isObject } from '@sniptt/guards'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { type PersonNode } from 'src/types/person-node'; + +export const readPeople = async ({ + client, + recordIds, +}: { + client: CoreApiClient; + recordIds: string[]; +}): Promise => { + if (recordIds.length === 0) { + return []; + } + + const result = (await client.query({ + people: { + __args: { filter: { id: { in: recordIds } }, first: recordIds.length }, + edges: { + node: { + id: true, + name: { firstName: true, lastName: true }, + emails: { primaryEmail: true }, + phones: { primaryPhoneNumber: true }, + jobTitle: true, + linkedinLink: { primaryLinkUrl: true }, + company: { id: true, name: true }, + pdlId: true, + pdlLastEnrichedAt: true, + }, + }, + }, + })) as { people?: { edges?: { node: PersonNode }[] } }; + + const edges = result.people?.edges; + if (!Array.isArray(edges)) { + return []; + } + + return edges + .map((edge) => edge?.node) + .filter( + (personNode): personNode is PersonNode => + isObject(personNode) && + isNonEmptyString((personNode as { id?: unknown }).id), + ); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/resolve-logic-function-id.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/resolve-logic-function-id.ts new file mode 100644 index 0000000000000..b527a230fecba --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/resolve-logic-function-id.ts @@ -0,0 +1,11 @@ +export const resolveLogicFunctionId = ({ + logicFunctions, + universalIdentifier, +}: { + logicFunctions: { id: string; universalIdentifier?: string | null }[]; + universalIdentifier: string; +}): string | undefined => + logicFunctions.find( + (logicFunction) => + logicFunction.universalIdentifier === universalIdentifier, + )?.id; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/resolve-min-likelihood.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/resolve-min-likelihood.ts new file mode 100644 index 0000000000000..f0f6515b484a7 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/resolve-min-likelihood.ts @@ -0,0 +1,12 @@ +const DEFAULT_MIN_LIKELIHOOD = 2; +const WEAK_IDENTIFIER_MIN_LIKELIHOOD = 6; + +export const resolveMinLikelihood = ({ + inputMinLikelihood, + hasStrongIdentifier, +}: { + inputMinLikelihood: number | undefined; + hasStrongIdentifier: boolean; +}): number => + inputMinLikelihood ?? + (hasStrongIdentifier ? DEFAULT_MIN_LIKELIHOOD : WEAK_IDENTIFIER_MIN_LIKELIHOOD); diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/run-batch-enrichment.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/run-batch-enrichment.ts new file mode 100644 index 0000000000000..7162a70d87f8b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/run-batch-enrichment.ts @@ -0,0 +1,50 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { aggregateBulkEnrichResult } from 'src/logic-functions/utils/aggregate-bulk-enrich-result'; +import { + buildErrorResult, + ENRICHMENT_FAILED_MESSAGE, +} from 'src/logic-functions/utils/build-error-result'; +import { chunk } from 'src/logic-functions/utils/chunk'; +import { enrichChunk } from 'src/logic-functions/utils/enrich-chunk'; +import { extractRecordIds } from 'src/logic-functions/utils/extract-record-ids'; +import { type BatchEnrichmentAdapter } from 'src/types/batch-enrichment-adapter'; +import { type BulkEnrichInput } from 'src/types/bulk-enrich-input'; +import { type BulkEnrichResult } from 'src/types/bulk-enrich-result'; +import { type CompanyIdByMatchKeyCache } from 'src/types/company-id-by-match-key-cache'; +import { type EnrichResult } from 'src/types/enrich-result'; + +const PDL_BATCH_SIZE = 100; + +export const runBatchEnrichment = async ({ + client, + input, + adapter, +}: { + client: CoreApiClient; + input: BulkEnrichInput; + adapter: BatchEnrichmentAdapter; +}): Promise => { + const recordIds = Array.from(new Set(extractRecordIds(input.records))); + const resultById = new Map(); + const companyIdByMatchKeyCache: CompanyIdByMatchKeyCache = new Map(); + + for (const recordIdsChunk of chunk({ items: recordIds, size: PDL_BATCH_SIZE })) { + await enrichChunk({ + client, + recordIds: recordIdsChunk, + input, + adapter, + resultById, + companyIdByMatchKeyCache, + }); + } + + const results = recordIds.map( + (recordId) => + resultById.get(recordId) ?? + buildErrorResult({ recordId, error: ENRICHMENT_FAILED_MESSAGE }), + ); + + return aggregateBulkEnrichResult(results); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/salary-transform.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/salary-transform.ts new file mode 100644 index 0000000000000..0ec2f050ff867 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/salary-transform.ts @@ -0,0 +1,18 @@ +import { stripSeparators } from 'src/utils/strip-separators'; + +const SALARY_LOOKUP: Record = { + '<20000': 'UNDER_20000', + '20000-25000': 'FROM_20000_TO_25000', + '25000-35000': 'FROM_25000_TO_35000', + '35000-45000': 'FROM_35000_TO_45000', + '45000-55000': 'FROM_45000_TO_55000', + '55000-70000': 'FROM_55000_TO_70000', + '70000-85000': 'FROM_70000_TO_85000', + '85000-100000': 'FROM_85000_TO_100000', + '100000-150000': 'FROM_100000_TO_150000', + '150000-250000': 'FROM_150000_TO_250000', + '>250000': 'OVER_250000', +}; + +export const salaryTransform = (raw: string): string | undefined => + SALARY_LOOKUP[stripSeparators(raw)]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/seed-enrichment-workflow.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/seed-enrichment-workflow.ts new file mode 100644 index 0000000000000..c71fcaddd7b75 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/seed-enrichment-workflow.ts @@ -0,0 +1,131 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { PdlOperationError } from 'src/logic-functions/errors/pdl-operation-error'; +import { buildBulkRecordsTrigger } from 'src/logic-functions/utils/build-bulk-records-trigger'; +import { buildLogicFunctionStep } from 'src/logic-functions/utils/build-logic-function-step'; +import { findExistingWorkflowId } from 'src/logic-functions/utils/find-existing-workflow-id'; +import { type EnrichmentWorkflowSeed } from 'src/types/enrichment-workflow-seed'; +import { type SeedEnrichmentWorkflowResult } from 'src/types/seed-enrichment-workflow-result'; +import { isDefined } from 'src/utils/is-defined'; + +const TRIGGER_STEP_ID = 'trigger'; + +export const seedEnrichmentWorkflow = async ({ + client, + logicFunctionId, + seed, +}: { + client: CoreApiClient; + logicFunctionId: string; + seed: EnrichmentWorkflowSeed; +}): Promise => { + const existingWorkflowId = await findExistingWorkflowId({ + client, + name: seed.workflowName, + }); + + if (isDefined(existingWorkflowId)) { + return { + objectNameSingular: seed.objectNameSingular, + workflowName: seed.workflowName, + status: 'skipped', + workflowId: existingWorkflowId, + }; + } + + const createResult = (await client.mutation({ + createWorkflow: { + __args: { data: { name: seed.workflowName } }, + id: true, + }, + })) as { createWorkflow?: { id?: string } }; + + const workflowId = createResult.createWorkflow?.id; + + if (!isDefined(workflowId)) { + throw new PdlOperationError( + `Failed to create workflow "${seed.workflowName}": no id returned.`, + ); + } + + const versionsResult = (await client.query({ + workflowVersions: { + __args: { filter: { workflowId: { eq: workflowId } } }, + edges: { node: { id: true, status: true } }, + }, + })) as { + workflowVersions?: { edges?: { node?: { id?: string; status?: string } }[] }; + }; + + const draftVersionId = versionsResult.workflowVersions?.edges?.find( + (edge) => edge.node?.status === 'DRAFT', + )?.node?.id; + + if (!isDefined(draftVersionId)) { + throw new PdlOperationError( + `No draft version found for workflow "${seed.workflowName}".`, + ); + } + + const stepId = crypto.randomUUID(); + + const trigger = buildBulkRecordsTrigger({ + objectNameSingular: seed.objectNameSingular, + name: seed.triggerName, + icon: seed.icon, + }); + + await client.mutation({ + updateWorkflowVersion: { + __args: { id: draftVersionId, data: { trigger } }, + id: true, + }, + }); + + const callMutationNotYetInClientSchema = ( + request: unknown, + ): Promise => client.mutation(request as never); + + await callMutationNotYetInClientSchema({ + createWorkflowVersionStep: { + __args: { + input: { + workflowVersionId: draftVersionId, + stepType: 'LOGIC_FUNCTION', + parentStepId: TRIGGER_STEP_ID, + id: stepId, + defaultSettings: { input: { logicFunctionId } }, + }, + }, + triggerDiff: true, + stepsDiff: true, + }, + }); + + const step = buildLogicFunctionStep({ + id: stepId, + name: seed.stepName, + logicFunctionId, + logicFunctionInput: seed.logicFunctionInput, + }); + + await callMutationNotYetInClientSchema({ + updateWorkflowVersionStep: { + __args: { input: { workflowVersionId: draftVersionId, step } }, + id: true, + }, + }); + + await callMutationNotYetInClientSchema({ + activateWorkflowVersion: { + __args: { workflowVersionId: draftVersionId }, + }, + }); + + return { + objectNameSingular: seed.objectNameSingular, + workflowName: seed.workflowName, + status: 'created', + workflowId, + }; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/size-transform.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/size-transform.ts new file mode 100644 index 0000000000000..585c36d664744 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/size-transform.ts @@ -0,0 +1,15 @@ +import { stripSeparators } from 'src/utils/strip-separators'; + +const SIZE_LOOKUP: Record = { + '1-10': 'ONE_TO_TEN', + '11-50': 'ELEVEN_TO_FIFTY', + '51-200': 'FIFTY_ONE_TO_TWO_HUNDRED', + '201-500': 'TWO_HUNDRED_ONE_TO_FIVE_HUNDRED', + '501-1000': 'FIVE_HUNDRED_ONE_TO_ONE_THOUSAND', + '1001-5000': 'ONE_THOUSAND_ONE_TO_FIVE_THOUSAND', + '5001-10000': 'FIVE_THOUSAND_ONE_TO_TEN_THOUSAND', + '10001+': 'TEN_THOUSAND_ONE_PLUS', +}; + +export const sizeTransform = (raw: string): string | undefined => + SIZE_LOOKUP[stripSeparators(raw)]; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-json-array.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-json-array.ts new file mode 100644 index 0000000000000..9722b5a9d8691 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-json-array.ts @@ -0,0 +1,4 @@ +import { isNonEmptyArray } from '@sniptt/guards'; + +export const toJsonArray = (value: unknown): unknown[] | undefined => + isNonEmptyArray(value) ? value : undefined; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-json-object.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-json-object.ts new file mode 100644 index 0000000000000..6d988ca763260 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-json-object.ts @@ -0,0 +1,22 @@ +import { isArray, isObject } from '@sniptt/guards'; + +export const toJsonObject = ( + value: unknown, +): Record | undefined => { + if (!isObject(value) || isArray(value)) { + return undefined; + } + + const valuePrototype = Object.getPrototypeOf(value); + const isPlainObject = + valuePrototype === Object.prototype || valuePrototype === null; + if (!isPlainObject) { + return undefined; + } + + if (Object.keys(value).length === 0) { + return undefined; + } + + return value as Record; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-number.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-number.ts new file mode 100644 index 0000000000000..cc067a7e4f8de --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-number.ts @@ -0,0 +1,4 @@ +import { isNumber } from '@sniptt/guards'; + +export const toNumber = (value: unknown): number | undefined => + isNumber(value) && Number.isFinite(value) ? value : undefined; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-string-array.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-string-array.ts new file mode 100644 index 0000000000000..22ad424095409 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-string-array.ts @@ -0,0 +1,28 @@ +import { isArray, isNonEmptyArray } from '@sniptt/guards'; + +import { toText } from 'src/logic-functions/utils/to-text'; +import { isDefined } from 'src/utils/is-defined'; + +export const toStringArray = (value: unknown): string[] | undefined => { + if (!isArray(value)) { + return undefined; + } + + const uniqueStrings: string[] = []; + const seenLowercaseValues = new Set(); + + for (const entry of value) { + const stringValue = toText(entry); + if (!isDefined(stringValue)) { + continue; + } + + const lowercaseValue = stringValue.toLowerCase(); + if (!seenLowercaseValues.has(lowercaseValue)) { + seenLowercaseValues.add(lowercaseValue); + uniqueStrings.push(stringValue); + } + } + + return isNonEmptyArray(uniqueStrings) ? uniqueStrings : undefined; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-text.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-text.ts new file mode 100644 index 0000000000000..cc70af7f0ae4f --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/to-text.ts @@ -0,0 +1,11 @@ +import { isNonEmptyString, isString } from '@sniptt/guards'; + +export const toText = (value: unknown): string | undefined => { + if (!isString(value)) { + return undefined; + } + + const trimmed = value.trim(); + + return isNonEmptyString(trimmed) ? trimmed : undefined; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-companies-status.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-companies-status.ts new file mode 100644 index 0000000000000..7a376b0dc5099 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-companies-status.ts @@ -0,0 +1,18 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +export const updateCompaniesStatus = async ({ + client, + recordIds, + data, +}: { + client: CoreApiClient; + recordIds: string[]; + data: Record; +}): Promise => { + await client.mutation({ + updateCompanies: { + __args: { filter: { id: { in: recordIds } }, data }, + id: true, + }, + }); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-company-record.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-company-record.ts new file mode 100644 index 0000000000000..eea3fc72450ed --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-company-record.ts @@ -0,0 +1,15 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +export const updateCompanyRecord = async ({ + client, + recordId, + data, +}: { + client: CoreApiClient; + recordId: string; + data: Record; +}): Promise => { + await client.mutation({ + updateCompany: { __args: { id: recordId, data }, id: true }, + }); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-people-status.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-people-status.ts new file mode 100644 index 0000000000000..2123164e19c6f --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-people-status.ts @@ -0,0 +1,18 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +export const updatePeopleStatus = async ({ + client, + recordIds, + data, +}: { + client: CoreApiClient; + recordIds: string[]; + data: Record; +}): Promise => { + await client.mutation({ + updatePeople: { + __args: { filter: { id: { in: recordIds } }, data }, + id: true, + }, + }); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-person-record.ts b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-person-record.ts new file mode 100644 index 0000000000000..237725211e79c --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/logic-functions/utils/update-person-record.ts @@ -0,0 +1,15 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +export const updatePersonRecord = async ({ + client, + recordId, + data, +}: { + client: CoreApiClient; + recordId: string; + data: Record; +}): Promise => { + await client.mutation({ + updatePerson: { __args: { id: recordId, data }, id: true }, + }); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/navigation-menu-items/enriched-companies.navigation-menu-item.ts b/packages/twenty-apps/internal/people-data-labs/src/navigation-menu-items/enriched-companies.navigation-menu-item.ts new file mode 100644 index 0000000000000..3e4a41a8df9ef --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/navigation-menu-items/enriched-companies.navigation-menu-item.ts @@ -0,0 +1,17 @@ +import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define'; + +import { + PDL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIERS, + PDL_VIEW_UNIVERSAL_IDENTIFIERS, +} from 'src/constants/universal-identifiers'; + +export default defineNavigationMenuItem({ + universalIdentifier: + PDL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIERS.enrichedCompanies, + type: NavigationMenuItemType.VIEW, + icon: 'IconSparkles', + position: 1, + folderUniversalIdentifier: + PDL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIERS.folder, + viewUniversalIdentifier: PDL_VIEW_UNIVERSAL_IDENTIFIERS.enrichedCompanies, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/navigation-menu-items/enriched-people.navigation-menu-item.ts b/packages/twenty-apps/internal/people-data-labs/src/navigation-menu-items/enriched-people.navigation-menu-item.ts new file mode 100644 index 0000000000000..375ba7e7b4beb --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/navigation-menu-items/enriched-people.navigation-menu-item.ts @@ -0,0 +1,17 @@ +import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define'; + +import { + PDL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIERS, + PDL_VIEW_UNIVERSAL_IDENTIFIERS, +} from 'src/constants/universal-identifiers'; + +export default defineNavigationMenuItem({ + universalIdentifier: + PDL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIERS.enrichedPeople, + type: NavigationMenuItemType.VIEW, + icon: 'IconSparkles', + position: 0, + folderUniversalIdentifier: + PDL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIERS.folder, + viewUniversalIdentifier: PDL_VIEW_UNIVERSAL_IDENTIFIERS.enrichedPeople, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/navigation-menu-items/people-data-labs-folder.navigation-menu-item.ts b/packages/twenty-apps/internal/people-data-labs/src/navigation-menu-items/people-data-labs-folder.navigation-menu-item.ts new file mode 100644 index 0000000000000..101245ed75b36 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/navigation-menu-items/people-data-labs-folder.navigation-menu-item.ts @@ -0,0 +1,14 @@ +import { + NavigationMenuItemType, + defineNavigationMenuItem, +} from 'twenty-sdk/define'; + +import { PDL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers'; + +export default defineNavigationMenuItem({ + universalIdentifier: PDL_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIERS.folder, + type: NavigationMenuItemType.FOLDER, + name: 'People Data Labs', + icon: 'IconSparkles', + position: 0, +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/roles/default-function.role.ts b/packages/twenty-apps/internal/people-data-labs/src/roles/default-function.role.ts index 9b0d4526cc2fc..a0169c9bbe1c8 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/roles/default-function.role.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/roles/default-function.role.ts @@ -1,6 +1,7 @@ import { defineApplicationRole, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, + SystemPermissionFlag, } from 'twenty-sdk/define'; import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers'; @@ -36,4 +37,5 @@ export default defineApplicationRole({ }, ], fieldPermissions: [], + permissionFlagUniversalIdentifiers: [SystemPermissionFlag.WORKFLOWS], }); diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/address-parts.ts b/packages/twenty-apps/internal/people-data-labs/src/types/address-parts.ts new file mode 100644 index 0000000000000..89a01bc614fa9 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/address-parts.ts @@ -0,0 +1,9 @@ +export type AddressParts = { + street1?: string | null; + street2?: string | null; + city?: string | null; + postcode?: string | null; + state?: string | null; + country?: string | null; + geo?: string | null; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/address-value.ts b/packages/twenty-apps/internal/people-data-labs/src/types/address-value.ts new file mode 100644 index 0000000000000..27dae09cde4da --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/address-value.ts @@ -0,0 +1,10 @@ +export type AddressValue = { + addressStreet1: string; + addressStreet2: string; + addressCity: string; + addressPostcode: string; + addressState: string; + addressCountry: string; + addressLat: number | null; + addressLng: number | null; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/batch-enrichment-adapter.ts b/packages/twenty-apps/internal/people-data-labs/src/types/batch-enrichment-adapter.ts new file mode 100644 index 0000000000000..7d96bcb6dc2a1 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/batch-enrichment-adapter.ts @@ -0,0 +1,38 @@ +import { type CoreApiClient } from 'twenty-client-sdk/core'; + +import { type BulkEnrichInput } from 'src/types/bulk-enrich-input'; +import { type CompanyIdByMatchKeyCache } from 'src/types/company-id-by-match-key-cache'; +import { type PdlEnrichResult } from 'src/types/pdl-enrich-result'; + +export type BatchEnrichmentAdapter = { + objectNameSingular: string; + noIdentifierMessage: string; + readRecords: (args: { + client: CoreApiClient; + recordIds: string[]; + }) => Promise; + getNodeId: (node: TNode) => string; + extractParams: (args: { + node: TNode; + input: BulkEnrichInput; + }) => TParams | undefined; + enrichBatch: (params: TParams[]) => Promise[]>; + buildMatchedData: (args: { + client: CoreApiClient; + node: TNode; + outcome: { likelihood?: number; data: TData }; + enrichedAt: string; + companyIdByMatchKeyCache: CompanyIdByMatchKeyCache; + overrideExistingValues: boolean; + }) => Promise>; + updateOne: (args: { + client: CoreApiClient; + recordId: string; + data: Record; + }) => Promise; + updateManyStatus: (args: { + client: CoreApiClient; + recordIds: string[]; + data: Record; + }) => Promise; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/bulk-enrich-input.ts b/packages/twenty-apps/internal/people-data-labs/src/types/bulk-enrich-input.ts new file mode 100644 index 0000000000000..b634508645d80 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/bulk-enrich-input.ts @@ -0,0 +1,7 @@ +export type RecordInput = string | { id?: string | null }; + +export type BulkEnrichInput = { + records: RecordInput | RecordInput[]; + overrideExistingValues?: boolean; + minLikelihood?: number; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/bulk-enrich-result.ts b/packages/twenty-apps/internal/people-data-labs/src/types/bulk-enrich-result.ts new file mode 100644 index 0000000000000..8db5828916759 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/bulk-enrich-result.ts @@ -0,0 +1,11 @@ +import { type EnrichResult } from 'src/types/enrich-result'; + +export type BulkEnrichResult = { + success: boolean; + total: number; + matched: number; + notFound: number; + skipped: number; + errored: number; + results: EnrichResult[]; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/company-id-by-match-key-cache.ts b/packages/twenty-apps/internal/people-data-labs/src/types/company-id-by-match-key-cache.ts new file mode 100644 index 0000000000000..3fc9e376684c8 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/company-id-by-match-key-cache.ts @@ -0,0 +1 @@ +export type CompanyIdByMatchKeyCache = Map; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/company-match-keys.ts b/packages/twenty-apps/internal/people-data-labs/src/types/company-match-keys.ts new file mode 100644 index 0000000000000..4c7c14577a0c8 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/company-match-keys.ts @@ -0,0 +1,6 @@ +export type CompanyMatchKeys = { + pdlId?: string; + website?: string; + linkedinUrl?: string; + name?: string; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/company-node.ts b/packages/twenty-apps/internal/people-data-labs/src/types/company-node.ts new file mode 100644 index 0000000000000..b9ea5a221fd79 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/company-node.ts @@ -0,0 +1,16 @@ +export type CompanyNode = { + id: string; + name?: string | null; + domainName?: { primaryLinkUrl?: string | null } | null; + linkedinLink?: { primaryLinkUrl?: string | null } | null; + address?: { + addressStreet1?: string | null; + addressStreet2?: string | null; + addressCity?: string | null; + addressPostcode?: string | null; + addressState?: string | null; + addressCountry?: string | null; + } | null; + pdlId?: string | null; + pdlLastEnrichedAt?: string | null; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/currency-value.ts b/packages/twenty-apps/internal/people-data-labs/src/types/currency-value.ts new file mode 100644 index 0000000000000..367e2d947900b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/currency-value.ts @@ -0,0 +1,4 @@ +export type CurrencyValue = { + amountMicros: number; + currencyCode: string; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/emails-value.ts b/packages/twenty-apps/internal/people-data-labs/src/types/emails-value.ts new file mode 100644 index 0000000000000..6b8b3e9dbdc56 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/emails-value.ts @@ -0,0 +1,4 @@ +export type EmailsValue = { + primaryEmail: string; + additionalEmails: string[] | null; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/enrich-result.ts b/packages/twenty-apps/internal/people-data-labs/src/types/enrich-result.ts new file mode 100644 index 0000000000000..012c54b690dc9 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/enrich-result.ts @@ -0,0 +1,10 @@ +import { type EnrichStatus } from 'src/types/enrich-status'; + +export type EnrichResult = { + success: boolean; + recordId: string; + status: EnrichStatus; + updatedFields: string[]; + message: string; + error?: string; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/enrich-status.ts b/packages/twenty-apps/internal/people-data-labs/src/types/enrich-status.ts new file mode 100644 index 0000000000000..355f7c371df47 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/enrich-status.ts @@ -0,0 +1 @@ +export type EnrichStatus = 'MATCHED' | 'NOT_FOUND' | 'ERROR' | 'SKIPPED'; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/enrich-tool-input.ts b/packages/twenty-apps/internal/people-data-labs/src/types/enrich-tool-input.ts new file mode 100644 index 0000000000000..90c4799b73ecf --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/enrich-tool-input.ts @@ -0,0 +1,5 @@ +export type EnrichToolInput = { + recordId?: string; + recordIds?: string[]; + overrideExistingValues?: boolean; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/enrichment-workflow-seed.ts b/packages/twenty-apps/internal/people-data-labs/src/types/enrichment-workflow-seed.ts new file mode 100644 index 0000000000000..f0cbd0b04924f --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/enrichment-workflow-seed.ts @@ -0,0 +1,9 @@ +export type EnrichmentWorkflowSeed = { + objectNameSingular: string; + workflowName: string; + triggerName: string; + icon: string; + stepName: string; + logicFunctionUniversalIdentifier: string; + logicFunctionInput: Record; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/full-name-value.ts b/packages/twenty-apps/internal/people-data-labs/src/types/full-name-value.ts new file mode 100644 index 0000000000000..cdf36da6e2ea1 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/full-name-value.ts @@ -0,0 +1,4 @@ +export type FullNameValue = { + firstName: string; + lastName: string; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/links-value.ts b/packages/twenty-apps/internal/people-data-labs/src/types/links-value.ts new file mode 100644 index 0000000000000..a26dc414384b3 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/links-value.ts @@ -0,0 +1,5 @@ +export type LinksValue = { + primaryLinkUrl: string; + primaryLinkLabel: string; + secondaryLinks: null; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/mapped-record.ts b/packages/twenty-apps/internal/people-data-labs/src/types/mapped-record.ts new file mode 100644 index 0000000000000..0897b16c7d3cd --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/mapped-record.ts @@ -0,0 +1,4 @@ +export type MappedRecord = { + standard: Record; + pdl: Record; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/pdl-company-data.ts b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-company-data.ts new file mode 100644 index 0000000000000..3c28c4da61c1d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-company-data.ts @@ -0,0 +1,50 @@ +export type PdlCompanyData = { + id?: string | null; + name?: string | null; + display_name?: string | null; + legal_name?: string | null; + alternative_names?: string[] | null; + alternative_domains?: string[] | null; + + website?: string | null; + linkedin_url?: string | null; + linkedin_id?: string | null; + facebook_url?: string | null; + twitter_url?: string | null; + profiles?: string[] | null; + + industry?: string | null; + industry_v2?: string | null; + naics?: unknown[] | null; + sic?: unknown[] | null; + tags?: string[] | null; + type?: string | null; + ticker?: string | null; + mic_exchange?: string | null; + + size?: string | null; + employee_count?: number | null; + employee_count_by_country?: Record | null; + founded?: number | null; + + summary?: string | null; + headline?: string | null; + + total_funding_raised?: number | null; + latest_funding_stage?: string | null; + funding_stages?: string[] | null; + last_funding_date?: string | null; + number_funding_rounds?: number | null; + + location?: { + street_address?: string | null; + address_line_2?: string | null; + locality?: string | null; + region?: string | null; + postal_code?: string | null; + country?: string | null; + continent?: string | null; + metro?: string | null; + geo?: string | null; + } | null; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/pdl-company-enrich-params.ts b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-company-enrich-params.ts new file mode 100644 index 0000000000000..ce8a61ee0a0e0 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-company-enrich-params.ts @@ -0,0 +1,7 @@ +export type PdlCompanyEnrichParams = { + pdlId?: string; + website?: string; + profile?: string; + name?: string; + minLikelihood?: number; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/pdl-enrich-result.ts b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-enrich-result.ts new file mode 100644 index 0000000000000..0ae6329b5535c --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-enrich-result.ts @@ -0,0 +1,4 @@ +export type PdlEnrichResult = + | { outcome: 'matched'; httpStatus: number; likelihood?: number; data: TData } + | { outcome: 'not_found'; httpStatus: number } + | { outcome: 'error'; httpStatus: number; message: string }; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/pdl-person-data.ts b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-person-data.ts new file mode 100644 index 0000000000000..9030c547aac0b --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-person-data.ts @@ -0,0 +1,65 @@ +export type PdlPersonData = { + id?: string | null; + full_name?: string | null; + first_name?: string | null; + last_name?: string | null; + name_aliases?: string[] | null; + + work_email?: string | null; + personal_emails?: string[] | null; + emails?: ({ address?: string | null } | string)[] | null; + recommended_personal_email?: string | null; + + mobile_phone?: string | null; + phone_numbers?: string[] | null; + + job_title?: string | null; + job_title_role?: string | null; + job_title_sub_role?: string | null; + job_title_class?: string | null; + job_title_levels?: string[] | null; + job_onet_code?: string | null; + job_summary?: string | null; + job_start_date?: string | null; + inferred_years_experience?: number | null; + inferred_salary?: string | null; + industry?: string | null; + + job_company_id?: string | null; + job_company_name?: string | null; + job_company_website?: string | null; + job_company_linkedin_url?: string | null; + job_company_industry?: string | null; + job_company_size?: string | null; + + linkedin_url?: string | null; + linkedin_username?: string | null; + linkedin_connections?: number | null; + facebook_url?: string | null; + twitter_url?: string | null; + github_url?: string | null; + + headline?: string | null; + summary?: string | null; + skills?: string[] | null; + interests?: string[] | null; + experience?: unknown[] | null; + education?: unknown[] | null; + certifications?: unknown[] | null; + profiles?: unknown[] | null; + languages?: unknown[] | null; + + sex?: string | null; + birth_year?: number | null; + birth_date?: string | null; + + location_name?: string | null; + location_street_address?: string | null; + location_address_line_2?: string | null; + location_locality?: string | null; + location_region?: string | null; + location_postal_code?: string | null; + location_country?: string | null; + location_metro?: string | null; + location_geo?: string | null; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/pdl-person-enrich-params.ts b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-person-enrich-params.ts new file mode 100644 index 0000000000000..3d1a485b1663d --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/pdl-person-enrich-params.ts @@ -0,0 +1,8 @@ +export type PdlPersonEnrichParams = { + pdlId?: string; + profile?: string; + email?: string; + name?: string; + company?: string; + minLikelihood?: number; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/person-node.ts b/packages/twenty-apps/internal/people-data-labs/src/types/person-node.ts new file mode 100644 index 0000000000000..7cce4c361cb68 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/person-node.ts @@ -0,0 +1,11 @@ +export type PersonNode = { + id: string; + name?: { firstName?: string | null; lastName?: string | null } | null; + emails?: { primaryEmail?: string | null } | null; + phones?: { primaryPhoneNumber?: string | null } | null; + jobTitle?: string | null; + linkedinLink?: { primaryLinkUrl?: string | null } | null; + company?: { id?: string | null; name?: string | null } | null; + pdlId?: string | null; + pdlLastEnrichedAt?: string | null; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/phones-value.ts b/packages/twenty-apps/internal/people-data-labs/src/types/phones-value.ts new file mode 100644 index 0000000000000..ad16fcfb2f764 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/phones-value.ts @@ -0,0 +1,12 @@ +export type AdditionalPhone = { + number: string; + countryCode: string; + callingCode: string; +}; + +export type PhonesValue = { + primaryPhoneNumber: string; + primaryPhoneCountryCode: string; + primaryPhoneCallingCode: string; + additionalPhones: AdditionalPhone[] | null; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/post-install-result.ts b/packages/twenty-apps/internal/people-data-labs/src/types/post-install-result.ts new file mode 100644 index 0000000000000..c2aaeb8ef42a3 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/post-install-result.ts @@ -0,0 +1,5 @@ +import { type SeedEnrichmentWorkflowResult } from 'src/types/seed-enrichment-workflow-result'; + +export type PostInstallResult = { + seededWorkflows: SeedEnrichmentWorkflowResult[]; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/seed-enrichment-workflow-result.ts b/packages/twenty-apps/internal/people-data-labs/src/types/seed-enrichment-workflow-result.ts new file mode 100644 index 0000000000000..d1368e0fbff44 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/seed-enrichment-workflow-result.ts @@ -0,0 +1,7 @@ +export type SeedEnrichmentWorkflowResult = { + objectNameSingular: string; + workflowName: string; + status: 'created' | 'skipped' | 'failed'; + workflowId?: string; + error?: string; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/select-option-meta.type.ts b/packages/twenty-apps/internal/people-data-labs/src/types/select-option-meta.ts similarity index 68% rename from packages/twenty-apps/internal/people-data-labs/src/types/select-option-meta.type.ts rename to packages/twenty-apps/internal/people-data-labs/src/types/select-option-meta.ts index 27e2a33f47a93..71296faedc093 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/types/select-option-meta.type.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/types/select-option-meta.ts @@ -1,4 +1,4 @@ -import { type TagColor } from 'src/types/tag-color.type'; +import { type TagColor } from 'src/types/tag-color'; export type SelectOptionMeta = { key: string; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/tag-color.type.ts b/packages/twenty-apps/internal/people-data-labs/src/types/tag-color.ts similarity index 100% rename from packages/twenty-apps/internal/people-data-labs/src/types/tag-color.type.ts rename to packages/twenty-apps/internal/people-data-labs/src/types/tag-color.ts diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/workflow-bulk-records-trigger.ts b/packages/twenty-apps/internal/people-data-labs/src/types/workflow-bulk-records-trigger.ts new file mode 100644 index 0000000000000..fb7e5ffa05042 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/workflow-bulk-records-trigger.ts @@ -0,0 +1,15 @@ +export type WorkflowBulkRecordsTrigger = { + name: string; + type: 'MANUAL'; + settings: { + objectType: string; + availability: { + type: 'BULK_RECORDS'; + objectNameSingular: string; + }; + outputSchema: Record; + icon: string; + isPinned: boolean; + }; + nextStepIds: string[]; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/types/workflow-logic-function-step.ts b/packages/twenty-apps/internal/people-data-labs/src/types/workflow-logic-function-step.ts new file mode 100644 index 0000000000000..993c099f32eb9 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/types/workflow-logic-function-step.ts @@ -0,0 +1,18 @@ +export type WorkflowLogicFunctionStep = { + id: string; + name: string; + type: 'LOGIC_FUNCTION'; + valid: boolean; + settings: { + input: { + logicFunctionId: string; + logicFunctionInput: Record; + }; + outputSchema: Record; + errorHandlingOptions: { + retryOnFailure: { value: boolean }; + continueOnFailure: { value: boolean }; + }; + }; + nextStepIds: string[]; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/build-select-options.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/build-select-options.spec.ts new file mode 100644 index 0000000000000..a019c8d6141eb --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/build-select-options.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; + +import { buildSelectOptions } from 'src/utils/build-select-options'; + +describe('buildSelectOptions', () => { + it('attaches the universalIdentifier to each option by key', () => { + const options = buildSelectOptions({ + meta: [ + { key: 'a', value: 'A', label: 'Option A', color: 'blue', position: 0 }, + { key: 'b', value: 'B', label: 'Option B', color: 'red', position: 1 }, + ], + ids: { a: 'id-a', b: 'id-b' }, + }); + + expect(options).toEqual([ + { id: 'id-a', value: 'A', label: 'Option A', color: 'blue', position: 0 }, + { id: 'id-b', value: 'B', label: 'Option B', color: 'red', position: 1 }, + ]); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/is-defined.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/is-defined.spec.ts new file mode 100644 index 0000000000000..908ea8e6aff09 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/is-defined.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { isDefined } from 'src/utils/is-defined'; + +describe('isDefined', () => { + it('is true for defined values, including falsy ones', () => { + expect(isDefined(0)).toBe(true); + expect(isDefined('')).toBe(true); + expect(isDefined(false)).toBe(true); + }); + + it('is false for null and undefined', () => { + expect(isDefined(null)).toBe(false); + expect(isDefined(undefined)).toBe(false); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/is-unique-violation-error.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/is-unique-violation-error.spec.ts new file mode 100644 index 0000000000000..0175d86108ee2 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/is-unique-violation-error.spec.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest'; + +import { isUniqueViolationError } from 'src/utils/is-unique-violation-error'; + +describe('isUniqueViolationError', () => { + it('detects common unique-violation phrasings (case-insensitive)', () => { + expect( + isUniqueViolationError( + new Error('duplicate key value violates unique constraint'), + ), + ).toBe(true); + expect(isUniqueViolationError(new Error('Record already exists'))).toBe( + true, + ); + expect(isUniqueViolationError({ message: 'Uniqueness violated' })).toBe( + true, + ); + expect(isUniqueViolationError('VIOLATES UNIQUE index')).toBe(true); + }); + + it('returns false for unrelated errors and non-error values', () => { + expect(isUniqueViolationError(new Error('network down'))).toBe(false); + expect(isUniqueViolationError(null)).toBe(false); + expect(isUniqueViolationError(undefined)).toBe(false); + expect(isUniqueViolationError(42)).toBe(false); + }); + + it("detects Twenty's user-friendly duplicate phrasing in graphQLErrors", () => { + expect( + isUniqueViolationError({ + message: 'Response not successful', + graphQLErrors: [ + { + message: 'A duplicate entry was detected', + extensions: { + code: 'BAD_USER_INPUT', + userFriendlyMessage: + 'This domain name value is already in use. Please check your data.', + }, + }, + ], + }), + ).toBe(true); + }); + + it('detects the structured DUPLICATE_ENTRY_DETECTED extensions code', () => { + expect( + isUniqueViolationError({ + message: 'whatever', + extensions: { code: 'DUPLICATE_ENTRY_DETECTED' }, + }), + ).toBe(true); + }); + + it('does not throw on a circular error object', () => { + const circular: Record = { message: 'boom' }; + circular.self = circular; + + expect(() => isUniqueViolationError(circular)).not.toThrow(); + expect(isUniqueViolationError(circular)).toBe(false); + }); + + it('finds the duplicate signal even on a circular error object', () => { + const circular: Record = { + message: 'duplicate key value', + }; + circular.self = circular; + + expect(isUniqueViolationError(circular)).toBe(true); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/normalize-enum-value.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/normalize-enum-value.spec.ts new file mode 100644 index 0000000000000..8838b75427370 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/normalize-enum-value.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; + +import { normalizeEnumValue } from 'src/utils/normalize-enum-value'; + +describe('normalizeEnumValue', () => { + it('uppercases a simple value', () => { + expect(normalizeEnumValue('engineering')).toBe('ENGINEERING'); + }); + + it('collapses spaces into a single underscore', () => { + expect(normalizeEnumValue('computer software')).toBe('COMPUTER_SOFTWARE'); + }); + + it('collapses punctuation runs into a single underscore', () => { + expect(normalizeEnumValue('airlines/aviation')).toBe('AIRLINES_AVIATION'); + expect(normalizeEnumValue('san francisco, california')).toBe( + 'SAN_FRANCISCO_CALIFORNIA', + ); + }); + + it('strips diacritics', () => { + expect(normalizeEnumValue('Montréal')).toBe('MONTREAL'); + }); + + it('prefixes a leading digit with an underscore', () => { + expect(normalizeEnumValue('3m')).toBe('_3M'); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/prune-undefined.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/prune-undefined.spec.ts new file mode 100644 index 0000000000000..118c841657776 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/prune-undefined.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; + +import { pruneUndefined } from 'src/utils/prune-undefined'; + +describe('pruneUndefined', () => { + it('removes keys whose value is undefined', () => { + expect(pruneUndefined({ a: 1, b: undefined, c: 3 })).toEqual({ a: 1, c: 3 }); + }); + + it('keeps falsy values that are not undefined', () => { + expect(pruneUndefined({ a: 0, b: '', c: null, d: false })).toEqual({ + a: 0, + b: '', + c: null, + d: false, + }); + }); + + it('returns an empty object when every value is undefined', () => { + expect(pruneUndefined({ a: undefined, b: undefined })).toEqual({}); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/strip-separators.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/strip-separators.spec.ts new file mode 100644 index 0000000000000..4d75e8241fe74 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/strip-separators.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { stripSeparators } from 'src/utils/strip-separators'; + +describe('stripSeparators', () => { + it('removes spaces and commas', () => { + expect(stripSeparators('45,000 - 55,000')).toBe('45000-55000'); + }); + + it('leaves a value without separators unchanged', () => { + expect(stripSeparators('1-10')).toBe('1-10'); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/to-error-message.spec.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/to-error-message.spec.ts new file mode 100644 index 0000000000000..0eaa94eedacc0 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/__tests__/to-error-message.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { toErrorMessage } from 'src/utils/to-error-message'; + +describe('toErrorMessage', () => { + it('returns the message of an Error', () => { + expect(toErrorMessage(new Error('boom'))).toBe('boom'); + }); + + it('stringifies a non-Error value', () => { + expect(toErrorMessage('oops')).toBe('oops'); + }); +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/build-select-options.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/build-select-options.ts index af2e35f982721..93fe715b00e36 100644 --- a/packages/twenty-apps/internal/people-data-labs/src/utils/build-select-options.ts +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/build-select-options.ts @@ -1,9 +1,12 @@ -import { type SelectOptionMeta } from 'src/types/select-option-meta.type'; +import { type SelectOptionMeta } from 'src/types/select-option-meta'; -export const buildSelectOptions = ( - meta: readonly SelectOptionMeta[], - ids: Record, -) => +export const buildSelectOptions = ({ + meta, + ids, +}: { + meta: readonly SelectOptionMeta[]; + ids: Record; +}) => meta.map(({ key, value, label, color, position }) => ({ id: ids[key], value, diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/is-defined.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/is-defined.ts new file mode 100644 index 0000000000000..aa97cf342fb91 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/is-defined.ts @@ -0,0 +1,5 @@ +import { isNull, isUndefined } from '@sniptt/guards'; + +export const isDefined = ( + value: T | null | undefined, +): value is NonNullable => !isUndefined(value) && !isNull(value); diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/is-unique-violation-error.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/is-unique-violation-error.ts new file mode 100644 index 0000000000000..68f8c1e339f76 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/is-unique-violation-error.ts @@ -0,0 +1,74 @@ +import { isObject } from '@sniptt/guards'; + +import { isDefined } from 'src/utils/is-defined'; + +const DUPLICATE_VIOLATION_PHRASES = [ + 'duplicate', + 'unique constraint', + 'uniqueness', + 'already exists', + 'is already in use', + 'violates unique', + 'duplicate_entry_detected', +]; + +const MAX_ERROR_TRAVERSAL_DEPTH = 4; + +const stringifyWithoutThrowingOnCycles = (value: unknown): string => { + try { + return JSON.stringify(value) ?? ''; + } catch { + return ''; + } +}; + +const collectErrorText = (error: unknown, depth = 0): string => { + if (!isDefined(error) || depth > MAX_ERROR_TRAVERSAL_DEPTH) { + return ''; + } + if (typeof error === 'string') { + return error; + } + if (typeof error !== 'object') { + return String(error); + } + + const errorObject = error as Record; + const textFragments: string[] = []; + + if (typeof errorObject.message === 'string') { + textFragments.push(errorObject.message); + } + + if (Array.isArray(errorObject.graphQLErrors)) { + for (const graphQLError of errorObject.graphQLErrors) { + textFragments.push(collectErrorText(graphQLError, depth + 1)); + } + } + + if (isObject(errorObject.extensions)) { + const extensions = errorObject.extensions as Record; + if (typeof extensions.code === 'string') { + textFragments.push(extensions.code); + } + if (typeof extensions.userFriendlyMessage === 'string') { + textFragments.push(extensions.userFriendlyMessage); + } + } + + if (isDefined(errorObject.cause)) { + textFragments.push(collectErrorText(errorObject.cause, depth + 1)); + } + + textFragments.push(stringifyWithoutThrowingOnCycles(error)); + + return textFragments.join(' '); +}; + +export const isUniqueViolationError = (error: unknown): boolean => { + const lowerCaseErrorText = collectErrorText(error).toLowerCase(); + + return DUPLICATE_VIOLATION_PHRASES.some((phrase) => + lowerCaseErrorText.includes(phrase), + ); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/normalize-enum-value.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/normalize-enum-value.ts new file mode 100644 index 0000000000000..46d68a8e98846 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/normalize-enum-value.ts @@ -0,0 +1,15 @@ +const DIACRITICS_REGEX = new RegExp('[\\u0300-\\u036f]', 'g'); +const NON_ALPHANUMERIC_RUN_REGEX = /[^A-Z0-9]+/g; +const LEADING_OR_TRAILING_UNDERSCORE_REGEX = /^_+|_+$/g; +const LEADING_DIGIT_REGEX = /^(\d)/; + +export const normalizeEnumValue = (rawEnumValue: string): string => { + const withoutDiacritics = rawEnumValue + .normalize('NFKD') + .replace(DIACRITICS_REGEX, ''); + const upperCased = withoutDiacritics.toUpperCase(); + const underscored = upperCased.replace(NON_ALPHANUMERIC_RUN_REGEX, '_'); + const trimmed = underscored.replace(LEADING_OR_TRAILING_UNDERSCORE_REGEX, ''); + + return trimmed.replace(LEADING_DIGIT_REGEX, '_$1'); +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/prune-undefined.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/prune-undefined.ts new file mode 100644 index 0000000000000..ed8911250e8fc --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/prune-undefined.ts @@ -0,0 +1,15 @@ +import { isUndefined } from '@sniptt/guards'; + +export const pruneUndefined = ( + record: Record, +): Record => { + const prunedRecord: Record = {}; + + for (const [key, value] of Object.entries(record)) { + if (!isUndefined(value)) { + prunedRecord[key] = value; + } + } + + return prunedRecord; +}; diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/strip-separators.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/strip-separators.ts new file mode 100644 index 0000000000000..2ef1e670d5f55 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/strip-separators.ts @@ -0,0 +1,4 @@ +const WHITESPACE_AND_COMMA_REGEX = /[\s,]/g; + +export const stripSeparators = (rawValue: string): string => + rawValue.replace(WHITESPACE_AND_COMMA_REGEX, ''); diff --git a/packages/twenty-apps/internal/people-data-labs/src/utils/to-error-message.ts b/packages/twenty-apps/internal/people-data-labs/src/utils/to-error-message.ts new file mode 100644 index 0000000000000..d9fa1691ab6d2 --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/utils/to-error-message.ts @@ -0,0 +1,2 @@ +export const toErrorMessage = (error: unknown): string => + error instanceof Error ? error.message : String(error); diff --git a/packages/twenty-apps/internal/people-data-labs/src/views/enriched-companies.view.ts b/packages/twenty-apps/internal/people-data-labs/src/views/enriched-companies.view.ts new file mode 100644 index 0000000000000..38cc08625539c --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/views/enriched-companies.view.ts @@ -0,0 +1,69 @@ +import { + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, + ViewType, + defineView, +} from 'twenty-sdk/define'; + +import { + PDL_FIELD_UNIVERSAL_IDENTIFIERS, + PDL_VIEW_UNIVERSAL_IDENTIFIERS, +} from 'src/constants/universal-identifiers'; + +export default defineView({ + universalIdentifier: PDL_VIEW_UNIVERSAL_IDENTIFIERS.enrichedCompanies, + name: 'Enriched (PDL)', + icon: 'IconSparkles', + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier, + type: ViewType.TABLE, + fields: [ + { universalIdentifier: 'cd791f00-a6d3-41b1-a75e-1a1343f0c091', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier, position: 0, isVisible: true }, + { universalIdentifier: '0ea4eaff-bc35-48f1-a36c-eecd4d947860', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.domainName.universalIdentifier, position: 1, isVisible: true }, + { universalIdentifier: '3aaeb807-a2a2-42cf-920b-3b52bbd395a7', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlIndustry, position: 2, isVisible: true }, + { universalIdentifier: '7bb72345-40b6-40cf-9024-8ac975b823fb', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlSizeRange, position: 3, isVisible: true }, + { universalIdentifier: '31e6a6ec-ce51-451b-9d1b-b7199d5af5cc', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlEmployeeCount, position: 4, isVisible: true }, + { universalIdentifier: '58221e25-4ec5-4397-b1da-e339870b37c7', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.annualRevenue.universalIdentifier, position: 5, isVisible: true }, + { universalIdentifier: '42675b10-c20a-4cc3-8876-3df9c70f71ee', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlTotalFunding, position: 6, isVisible: true }, + { universalIdentifier: 'd5a6570c-9f4d-4819-87db-a0e5fd026384', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLatestFundingStage, position: 7, isVisible: true }, + { universalIdentifier: '81aa25bf-13ac-43d0-b398-79f67df58d57', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLastFundingDate, position: 8, isVisible: true }, + { universalIdentifier: '2d4ca41d-a281-47ee-9f94-d1100fa8b1b4', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlNumberFundingRounds, position: 9, isVisible: true }, + { universalIdentifier: '5a8dc89b-d844-4ac4-81ad-fb3eeee37b19', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlFundingStages, position: 10, isVisible: true }, + { universalIdentifier: '882d75b3-d2fe-46a5-b469-e317fe28f4b5', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlFoundedYear, position: 11, isVisible: true }, + { universalIdentifier: '9220869b-d830-4be4-acc4-4f649e6f999a', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlCompanyType, position: 12, isVisible: true }, + { universalIdentifier: 'ca4f29d6-943a-4465-b2ce-19170ba69f57', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlIndustryDetail, position: 13, isVisible: true }, + { universalIdentifier: '8d6eb925-ac30-4cfb-87c1-8c1a8b1f380a', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlHeadline, position: 14, isVisible: true }, + { universalIdentifier: 'e6fa2cbe-2326-42d2-9715-7a3fca5584eb', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.linkedinLink.universalIdentifier, position: 15, isVisible: true }, + { universalIdentifier: 'db77ac92-03d1-4145-894d-1bf3f0d9c859', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLocationMetro, position: 16, isVisible: true }, + { universalIdentifier: 'ce8c6d73-067f-4129-a277-45bd51dcb357', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLocationContinent, position: 17, isVisible: true }, + { universalIdentifier: '420e3215-9f33-46b4-958a-296a6d242fbe', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlEnrichmentStatus, position: 18, isVisible: true }, + { universalIdentifier: '7a6f4e49-4843-4521-94b1-beb28787e5bf', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLastEnrichedAt, position: 19, isVisible: true }, + { universalIdentifier: '1cf22bd5-aae9-46c7-b76f-23cb72478c99', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.accountOwner.universalIdentifier, position: 20, isVisible: true }, + { universalIdentifier: '1883e773-b56e-4f76-a204-9da842c64dc9', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.people.universalIdentifier, position: 21, isVisible: true }, + { universalIdentifier: '335ee984-253b-4ae1-a50a-2d36152c3864', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.opportunities.universalIdentifier, position: 22, isVisible: true }, + { universalIdentifier: '03336c71-3ffb-407b-a92b-582d9a3f98bc', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlSummary, position: 23, isVisible: true }, + { universalIdentifier: 'a7f10fcd-8a94-4dc4-9367-6cd5cbb9be04', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlTicker, position: 24, isVisible: true }, + { universalIdentifier: '4b340b27-788c-42b1-a605-99dd911b650a', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlMicExchange, position: 25, isVisible: true }, + { universalIdentifier: '538dd51c-c41e-4d8e-b114-3def4505d61d', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.address.universalIdentifier, position: 26, isVisible: true }, + { universalIdentifier: 'fcdb49ca-cc24-4e3b-bd34-b58580374337', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLinkedinId, position: 27, isVisible: true }, + { universalIdentifier: '04dd488f-69eb-4073-876e-d69b433900b5', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlTwitterUrl, position: 28, isVisible: true }, + { universalIdentifier: '450f706c-57bc-47f9-a67b-4cd9cb19deda', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlFacebookUrl, position: 29, isVisible: true }, + { universalIdentifier: '775f7dfd-4d90-4da5-88fe-d800e577ac28', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlId, position: 30, isVisible: true }, + { universalIdentifier: '05db98e3-3532-45e2-895a-3455aa9bd609', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlAlternativeNames, position: 31, isVisible: true }, + { universalIdentifier: '0c027ae2-7e63-4354-8962-6922923929c0', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlAlternativeDomains, position: 32, isVisible: true }, + { universalIdentifier: '0814427e-6699-44fd-9d57-7af6f2389f7d', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlTags, position: 33, isVisible: true }, + { universalIdentifier: '65ca68c9-589f-44dc-bf57-d0a2152cb09c', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlAffiliatedProfiles, position: 34, isVisible: true }, + { universalIdentifier: '3f0e5a60-3f49-4872-8451-5344a3715374', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.noteTargets.universalIdentifier, position: 35, isVisible: true }, + { universalIdentifier: '70ab74e1-d2ff-4f15-81ef-196e223a783e', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.taskTargets.universalIdentifier, position: 36, isVisible: true }, + { universalIdentifier: '035b3904-59b9-4fce-a366-ee0139770346', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.attachments.universalIdentifier, position: 37, isVisible: true }, + { universalIdentifier: '617b7343-1ae5-4813-b4f5-6e99d0a833bb', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.timelineActivities.universalIdentifier, position: 38, isVisible: true }, + { universalIdentifier: '24d0411e-23d2-4cf8-9781-6efc8443efdc', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdBy.universalIdentifier, position: 39, isVisible: true }, + { universalIdentifier: '4036071d-e527-46c8-940c-401f33803aa9', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.updatedBy.universalIdentifier, position: 40, isVisible: true }, + { universalIdentifier: 'ab123fcd-ec53-4d33-b43c-c7319db72be4', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.createdAt.universalIdentifier, position: 41, isVisible: true }, + { universalIdentifier: 'f4c2ee74-f9b7-43f6-8bc9-1ca8ffe92a9f', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.updatedAt.universalIdentifier, position: 42, isVisible: true }, + { universalIdentifier: 'a7d07429-0c5b-4b04-a752-a243db5fe92d', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlNaics, position: 43, isVisible: true }, + { universalIdentifier: 'fe01f70a-e281-4d2e-8674-580db30f6c96', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlSic, position: 44, isVisible: true }, + { universalIdentifier: '7a69eeb6-c74b-40a8-b692-28f0fc289f42', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlEmployeeCountByCountry, position: 45, isVisible: true }, + { universalIdentifier: 'aa9ac27c-8281-4a8e-ae32-33ccad4e5207', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLegalName, position: 46, isVisible: true }, + { universalIdentifier: 'd647bf86-f3e6-44ae-8bb3-68d444ca7608', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlRawPayload, position: 47, isVisible: true }, + ], +}); diff --git a/packages/twenty-apps/internal/people-data-labs/src/views/enriched-people.view.ts b/packages/twenty-apps/internal/people-data-labs/src/views/enriched-people.view.ts new file mode 100644 index 0000000000000..53a2052c6102c --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/src/views/enriched-people.view.ts @@ -0,0 +1,74 @@ +import { + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, + ViewType, + defineView, +} from 'twenty-sdk/define'; + +import { + PDL_FIELD_UNIVERSAL_IDENTIFIERS, + PDL_VIEW_UNIVERSAL_IDENTIFIERS, +} from 'src/constants/universal-identifiers'; + +export default defineView({ + universalIdentifier: PDL_VIEW_UNIVERSAL_IDENTIFIERS.enrichedPeople, + name: 'Enriched (PDL)', + icon: 'IconSparkles', + objectUniversalIdentifier: + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, + type: ViewType.TABLE, + fields: [ + { universalIdentifier: 'b49f78fd-c110-4415-9edf-b6548ffebb11', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.name.universalIdentifier, position: 0, isVisible: true }, + { universalIdentifier: 'bf435e49-9752-4cbd-8d67-b236dddbe1f6', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.jobTitle.universalIdentifier, position: 1, isVisible: true }, + { universalIdentifier: 'cf6c19b0-ac25-4339-9b11-dc75956154f7', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.company.universalIdentifier, position: 2, isVisible: true }, + { universalIdentifier: '93c2b376-d453-454c-948c-cebeab7a7498', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlSeniority, position: 3, isVisible: true }, + { universalIdentifier: 'eedc86dc-2cb3-4dfc-a07f-5cfde2a4f9c7', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobRole, position: 4, isVisible: true }, + { universalIdentifier: '55a34eeb-5f00-4bd1-83c7-c5b9b1f23454', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobTitleClass, position: 5, isVisible: true }, + { universalIdentifier: '9b1f5443-c5ef-4557-9a8f-af25b943a4ab', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobTitleSubRole, position: 6, isVisible: true }, + { universalIdentifier: 'd24696dc-0e5f-4700-8af6-36b0c3334b00', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.emails.universalIdentifier, position: 7, isVisible: true }, + { universalIdentifier: '13313ccd-4cdf-43b3-8422-5de3c3d4d3c7', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.phones.universalIdentifier, position: 8, isVisible: true }, + { universalIdentifier: '0d0c13a7-fcaf-4273-835b-a31420d9f766', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.linkedinLink.universalIdentifier, position: 9, isVisible: true }, + { universalIdentifier: '8dbb042c-5a29-41c8-ae4d-cd8e22d64fc3', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlIndustry, position: 10, isVisible: true }, + { universalIdentifier: 'e50a7a85-32c9-47e2-87e8-ac5285078a53', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlYearsExperience, position: 11, isVisible: true }, + { universalIdentifier: '0c85e27c-5059-4d4e-b594-8c6d9c73bb54', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobStartDate, position: 12, isVisible: true }, + { universalIdentifier: 'f54ebb42-365c-4955-adc8-46900e3dc4a0', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlHeadline, position: 13, isVisible: true }, + { universalIdentifier: '28a1370a-d3dd-4f05-b9e1-caad9054d10c', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobSummary, position: 14, isVisible: true }, + { universalIdentifier: 'e2ebb0e0-6538-469b-8695-de223c46995a', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlInferredSalary, position: 15, isVisible: true }, + { universalIdentifier: 'c18429dd-3a12-4340-9c17-c6f9bce50595', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlSkills, position: 16, isVisible: true }, + { universalIdentifier: '72961760-44f8-4e70-b0b7-e1d86579bb91', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlEnrichmentStatus, position: 17, isVisible: true }, + { universalIdentifier: '4e3b0ecb-9167-4edd-b28b-c640d2a223f7', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlLastEnrichedAt, position: 18, isVisible: true }, + { universalIdentifier: 'e795ae5e-ab14-467d-823d-ca5206fed0d9', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlLikelihood, position: 19, isVisible: true }, + { universalIdentifier: 'cb00d87c-018d-4ddf-8fb6-d7d8bce56ecc', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlLocation, position: 20, isVisible: true }, + { universalIdentifier: '84cafc5f-a208-49dc-aa1a-854b48c93136', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlLocationMetro, position: 21, isVisible: true }, + { universalIdentifier: 'bf8c0b54-457f-4195-8d92-18344923203e', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlSummary, position: 22, isVisible: true }, + { universalIdentifier: 'd15bf0da-ae93-43b9-83db-248cdb7eb3dd', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlLinkedinUsername, position: 23, isVisible: true }, + { universalIdentifier: '42736d0e-6441-4c92-9843-5dc52c5d33d7', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlLinkedinConnections, position: 24, isVisible: true }, + { universalIdentifier: '2c1de05d-4df0-427a-bff9-87ab51c7f4e2', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlGithubUrl, position: 25, isVisible: true }, + { universalIdentifier: '973ad129-6188-40b4-8705-bdf9afb84725', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlTwitterUrl, position: 26, isVisible: true }, + { universalIdentifier: '4c540a8f-1921-41b0-81e1-af4f62d5c732', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlFacebookUrl, position: 27, isVisible: true }, + { universalIdentifier: '9659bd9d-eadc-4ada-9b0c-801ffbbdb58b', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.avatarUrl.universalIdentifier, position: 28, isVisible: true }, + { universalIdentifier: 'fcdff922-d83d-4c42-9349-cc88dd768d07', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlInterests, position: 29, isVisible: true }, + { universalIdentifier: 'f7d4c1ee-4b41-4432-9188-c83ff56e0ea6', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlJobOnetCode, position: 30, isVisible: true }, + { universalIdentifier: '3b8e9b28-6081-469a-9c3a-3f9fa399af92', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlId, position: 31, isVisible: true }, + { universalIdentifier: '98c1bfc5-ab29-4783-821b-714fa63a7b62', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.pointOfContactForOpportunities.universalIdentifier, position: 32, isVisible: true }, + { universalIdentifier: '9d312f87-17e2-4135-8b1e-1cc1099773ea', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.messageParticipants.universalIdentifier, position: 33, isVisible: true }, + { universalIdentifier: 'c69e8627-b85d-4e11-bb20-1d01b45e88d5', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.calendarEventParticipants.universalIdentifier, position: 34, isVisible: true }, + { universalIdentifier: 'b508225a-044b-4379-8233-e4de89a1a4f6', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlNameAliases, position: 35, isVisible: true }, + { universalIdentifier: '730dfb73-0ff7-429a-a70b-fefc6080b537', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlSex, position: 36, isVisible: true }, + { universalIdentifier: '1620bf1d-0b12-4f69-ad0e-ba7dd1d8aa44', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlBirthDate, position: 37, isVisible: true }, + { universalIdentifier: '029770cb-9353-4b5f-8395-b217cc95402e', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlBirthYear, position: 38, isVisible: true }, + { universalIdentifier: 'ef6e496c-de6c-47ed-bd44-886736f942ec', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.noteTargets.universalIdentifier, position: 39, isVisible: true }, + { universalIdentifier: '3758ed96-f76d-49a0-b37b-7c5c9aa4c1c1', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.taskTargets.universalIdentifier, position: 40, isVisible: true }, + { universalIdentifier: 'a5bf7046-8fc9-4704-91a5-ba6c1b0f4b58', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.attachments.universalIdentifier, position: 41, isVisible: true }, + { universalIdentifier: 'f6ed39c3-aff2-4ccf-b4e8-57de72b64cdd', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.timelineActivities.universalIdentifier, position: 42, isVisible: true }, + { universalIdentifier: '45159f0c-bac0-439c-ad8d-c7e4fce4b457', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.createdBy.universalIdentifier, position: 43, isVisible: true }, + { universalIdentifier: '8a5f1742-317f-4b01-b0c1-5d0866f78fa9', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedBy.universalIdentifier, position: 44, isVisible: true }, + { universalIdentifier: '1614c9fb-3566-481f-9dfb-bbba37b1c8aa', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.createdAt.universalIdentifier, position: 45, isVisible: true }, + { universalIdentifier: '01dfdc66-6df5-4b7a-8b11-63feabcb915d', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.fields.updatedAt.universalIdentifier, position: 46, isVisible: true }, + { universalIdentifier: 'bdf6af4b-d18c-4ddb-91a0-c011a1493d8c', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlExperience, position: 47, isVisible: true }, + { universalIdentifier: '55c8cd77-5871-4246-9793-bc353aab7734', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlEducation, position: 48, isVisible: true }, + { universalIdentifier: 'e513190d-3f4b-4585-b16a-133056553bc8', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlCertifications, position: 49, isVisible: true }, + { universalIdentifier: 'ced78962-8563-4503-b81f-9bdfde914deb', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlLanguages, position: 50, isVisible: true }, + { universalIdentifier: 'fb119419-75dd-4551-8f92-20e5b5e9e323', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlProfiles, position: 51, isVisible: true }, + { universalIdentifier: '034e7367-c882-4243-88df-16edf0c34bf7', fieldMetadataUniversalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlRawPayload, position: 52, isVisible: true }, + ], +}); diff --git a/packages/twenty-apps/internal/people-data-labs/vitest.unit.config.ts b/packages/twenty-apps/internal/people-data-labs/vitest.unit.config.ts new file mode 100644 index 0000000000000..3d0b80102c30f --- /dev/null +++ b/packages/twenty-apps/internal/people-data-labs/vitest.unit.config.ts @@ -0,0 +1,16 @@ +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +// Unit tests for the enrichment mapper (pure functions). Unlike the integration +// config, these run without a live Twenty server, so there is no globalSetup. +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + include: ['src/**/*.spec.ts'], + }, +});