fixes and sdk version bump

This commit is contained in:
bosiraphael
2026-06-05 18:38:49 +02:00
parent eafd31c919
commit d7c3f2282d
32 changed files with 932 additions and 425 deletions
@@ -27,8 +27,7 @@ trigger-agnostic core in `src/logic-functions/handlers/`:
Run locally: `yarn twenty dev:function:exec -n enrich-person -p '{"recordId":"<id>"}'`.
**Deferred to a later PR:** linking the `pdlCurrentCompany` relation (find-or-create a
Company from `job_company_*`), enrichment metering/billing, and auto-enrichment triggers.
**Deferred to a later PR:** enrichment metering/billing, and auto-enrichment triggers.
---
@@ -49,9 +48,9 @@ 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.
@@ -82,11 +81,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`
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.
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).
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
@@ -132,7 +139,8 @@ The logic function (to be built) must:
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`.
12. **Company**: resolve `job_company_*` → find-or-create a Company record → link the standard
`company` relation (fill-only-if-empty).
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).
@@ -21,8 +21,8 @@
"test:integration": "vitest run --passWithNoTests"
},
"dependencies": {
"twenty-client-sdk": "2.4.0",
"twenty-sdk": "2.4.0"
"twenty-client-sdk": "2.9.1",
"twenty-sdk": "2.9.1"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -28,7 +28,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',
@@ -47,12 +46,6 @@ export const PDL_FIELD_UNIVERSAL_IDENTIFIERS = {
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',
@@ -73,7 +66,6 @@ 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',
@@ -1097,155 +1089,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',
@@ -1523,16 +1366,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',
@@ -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',
});
@@ -1,27 +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,
joinColumnName: 'pdlCurrentCompanyId',
},
icon: 'IconBuilding',
});
@@ -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.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.',
icon: 'IconId',
isNullable: true,
});
@@ -1,30 +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.',
icon: 'IconBuildingFactory2',
isNullable: true,
options: buildSelectOptions(
INDUSTRY_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobCompanyIndustry,
),
});
@@ -1,20 +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.',
icon: 'IconBrandLinkedin',
isNullable: true,
});
@@ -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.pdlJobCompanyName,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlJobCompanyName',
label: 'Current Company',
description: 'Current company name returned by People Data Labs.',
icon: 'IconBuilding',
isNullable: true,
});
@@ -1,28 +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.',
icon: 'IconChartBar',
isNullable: true,
options: buildSelectOptions(
SIZE_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.personJobCompanySize,
),
});
@@ -1,20 +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.',
icon: 'IconWorld',
isNullable: true,
});
@@ -1,20 +1,34 @@
import { type CoreApiClient } from 'twenty-client-sdk/core';
import { vi } from 'vitest';
type Resolver<TResult> = TResult | ((request: unknown) => TResult);
type CoreApiClientMockOptions = {
queryResult?: unknown;
queryResult?: Resolver<unknown>;
mutationResult?: Resolver<unknown>;
onMutation?: (request: unknown) => void;
};
const resolve = <TResult>(
resolver: Resolver<TResult> | 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(() => Promise.resolve(queryResult));
const query = vi.fn((request: unknown) =>
Promise.resolve(resolve(queryResult, request)),
);
const mutation = vi.fn((request: unknown) => {
onMutation?.(request);
return Promise.resolve({});
return Promise.resolve(resolve(mutationResult, request) ?? {});
});
return { query, mutation } as unknown as CoreApiClient;
@@ -18,12 +18,16 @@ type MutationRequest = {
updatePerson: { __args: { data: Record<string, unknown> } };
};
const captureUpdatePerson = (captured: Captured) => (request: unknown) => {
if ('updatePerson' in (request as object)) {
captured.data = (request as MutationRequest).updatePerson.__args.data;
}
};
const buildClient = (node: PersonNode, captured: Captured) =>
createCoreApiClientMock({
queryResult: { people: { edges: [{ node }] } },
onMutation: (request) => {
captured.data = (request as MutationRequest).updatePerson.__args.data;
},
onMutation: captureUpdatePerson(captured),
});
beforeEach(() => {
@@ -62,6 +66,77 @@ describe('enrichPersonCore', () => {
expect('linkedinLink' in (captured.data ?? {})).toBe(false);
});
it('links the standard company (find-or-create) when the person has none', async () => {
enrichPersonMock.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 = createCoreApiClientMock({
queryResult: (request: unknown) =>
'companies' in (request as object)
? { companies: { edges: [] } }
: { people: { edges: [{ node: PERSON_NODE_MOCK }] } },
mutationResult: (request: unknown) =>
'createCompany' in (request as object)
? { createCompany: { id: 'co-new' } }
: {},
onMutation: captureUpdatePerson(captured),
});
await enrichPersonCore({ recordId: 'p1' }, client);
expect(captured.data?.companyId).toBe('co-new');
});
it('does not touch company when the person already has one', async () => {
enrichPersonMock.mockResolvedValue({
outcome: 'matched',
httpStatus: 200,
likelihood: 8,
data: { id: 'pdl1', work_email: 'jane@acme.com', job_company_name: 'Acme' },
});
const captured: Captured = {};
let companiesQueried = false;
let createCompanyCalled = false;
const client = createCoreApiClientMock({
queryResult: (request: unknown) => {
if ('companies' in (request as object)) {
companiesQueried = true;
return { companies: { edges: [] } };
}
return {
people: {
edges: [{ node: { ...PERSON_NODE_MOCK, company: { id: 'co-existing' } } }],
},
};
},
mutationResult: (request: unknown) => {
if ('createCompany' in (request as object)) {
createCompanyCalled = true;
}
return {};
},
onMutation: captureUpdatePerson(captured),
});
await enrichPersonCore({ recordId: 'p1' }, client);
expect(companiesQueried).toBe(false);
expect(createCompanyCalled).toBe(false);
expect('companyId' in (captured.data ?? {})).toBe(false);
});
it('does not overwrite a populated standard field', async () => {
enrichPersonMock.mockResolvedValue({
outcome: 'matched',
@@ -5,6 +5,7 @@ import { PdlInvalidInputError } from 'src/logic-functions/errors/pdl-invalid-inp
import { PdlRecordNotFoundError } from 'src/logic-functions/errors/pdl-record-not-found-error';
import { buildPersonNameParam } from 'src/logic-functions/utils/build-person-name-param';
import { buildSkippedResult } from 'src/logic-functions/utils/build-skipped-result';
import { findOrCreateCurrentCompany } from 'src/logic-functions/utils/find-or-create-current-company';
import { toText } from 'src/logic-functions/utils/to-text';
import { isWithinTtl } from 'src/logic-functions/utils/is-within-ttl';
import { mapPerson } from 'src/logic-functions/utils/map-person';
@@ -138,9 +139,14 @@ export const enrichPersonCore = async (
PERSON_EMPTY_CHECKS,
);
const currentCompanyId = isDefined(node.company?.id)
? undefined
: await findOrCreateCurrentCompany(client, outcome.data as PdlPersonData);
const data = pruneUndefined({
...writableStandard,
...mapped.pdl,
companyId: currentCompanyId,
pdlLikelihood: outcome.likelihood,
pdlRawPayload: outcome.data,
pdlLastEnrichedAt: enrichedAt,
@@ -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.type';
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: 'https://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' });
});
});
@@ -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.type';
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: 'https://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({});
});
});
@@ -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, { 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, { name: { eq: 'Acme' } }),
).toBeUndefined();
});
});
@@ -0,0 +1,90 @@
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<string, unknown> } };
};
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, 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, 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, { 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, 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, {})).toBeUndefined();
expect(query).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,106 @@
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';
type CompanyQueryRequest = {
companies: { __args: { filter: Record<string, unknown> } };
};
type CreateCompanyRequest = {
createCompany: { __args: { data: Record<string, unknown> } };
};
const filterOf = (request: unknown) =>
(request as CompanyQueryRequest).companies.__args.filter;
const createDataOf = (request: unknown) =>
(request as CreateCompanyRequest).createCompany.__args.data;
describe('findOrCreateCurrentCompany', () => {
it('returns undefined and issues no calls when PDL gives no name or website', async () => {
const query = vi.fn();
const mutation = vi.fn();
const client = { query, mutation } as unknown as CoreApiClient;
const result = await findOrCreateCurrentCompany(client, {
job_company_id: 'pdl-co-1',
} as PdlPersonData);
expect(result).toBeUndefined();
expect(query).not.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, {
job_company_name: 'Acme',
} as PdlPersonData);
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<string, unknown> | 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, {
job_company_id: 'pdl-co-2',
job_company_name: 'Acme',
job_company_website: 'acme.com',
} as PdlPersonData);
expect(result).toBe('co-new');
expect(createdData).toMatchObject({ name: 'Acme', pdlId: 'pdl-co-2' });
});
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, {
job_company_id: 'pdl-co-3',
job_company_name: 'Acme',
} as PdlPersonData);
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, {
job_company_name: 'Acme',
} as PdlPersonData),
).rejects.toThrow('network down');
});
});
@@ -25,7 +25,13 @@ describe('mapPerson', () => {
expect(pdl.pdlJobRole).toBe('ENGINEERING');
expect(pdl.pdlIndustry).toBe('ACCOUNTING');
expect(pdl.pdlInferredSalary).toBe('FROM_45000_TO_55000');
expect(pdl.pdlJobCompanySize).toBe('ELEVEN_TO_FIFTY');
});
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', () => {
@@ -0,0 +1,24 @@
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 { 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.type';
import { pruneUndefined } from 'src/utils/prune-undefined';
const INDUSTRY_VALUES = buildAllowedValues(INDUSTRY_OPTIONS);
const SIZE_VALUES = buildAllowedValues(SIZE_OPTIONS);
export const buildCompanyCreateData = (
data: PdlPersonData,
): Record<string, unknown> =>
pruneUndefined<unknown>({
name: toText(data.job_company_name),
domainName: buildLinks(data.job_company_website),
linkedinLink: buildLinks(data.job_company_linkedin_url),
pdlId: toText(data.job_company_id),
pdlIndustry: pickSelect(data.job_company_industry, INDUSTRY_VALUES),
pdlSizeRange: pickSelect(data.job_company_size, SIZE_VALUES, sizeTransform),
});
@@ -0,0 +1,12 @@
import { toText } from 'src/logic-functions/utils/to-text';
import { type CompanyMatchKeys } from 'src/types/company-match-keys.type';
import { type PdlPersonData } from 'src/types/pdl-person-data.type';
import { pruneUndefined } from 'src/utils/prune-undefined';
export const buildCompanyMatchKeys = (data: PdlPersonData): CompanyMatchKeys =>
pruneUndefined({
pdlId: toText(data.job_company_id),
website: toText(data.job_company_website),
linkedinUrl: toText(data.job_company_linkedin_url),
name: toText(data.job_company_name),
});
@@ -0,0 +1,17 @@
import { type CoreApiClient } from 'twenty-client-sdk/core';
type CompaniesConnection = { edges?: { node: { id: string } }[] };
export const findCompanyIdByFilter = async (
client: CoreApiClient,
filter: Record<string, unknown>,
): Promise<string | undefined> => {
const result = (await client.query({
companies: {
__args: { filter, first: 1 },
edges: { node: { id: true } },
},
})) as { companies?: CompaniesConnection };
return result.companies?.edges?.[0]?.node?.id;
};
@@ -0,0 +1,49 @@
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.type';
import { isDefined } from 'src/utils/is-defined';
export const findCompanyId = async (
client: CoreApiClient,
{ pdlId, website, linkedinUrl, name }: CompanyMatchKeys,
): Promise<string | undefined> => {
if (isNonEmptyString(pdlId)) {
const matchById = await findCompanyIdByFilter(client, {
pdlId: { eq: pdlId },
});
if (isDefined(matchById)) {
return matchById;
}
}
if (isNonEmptyString(website)) {
const matchByDomain = await findCompanyIdByFilter(client, {
domainName: { primaryLinkUrl: { eq: website } },
});
if (isDefined(matchByDomain)) {
return matchByDomain;
}
}
if (isNonEmptyString(linkedinUrl)) {
const matchByLinkedin = await findCompanyIdByFilter(client, {
linkedinLink: { primaryLinkUrl: { eq: linkedinUrl } },
});
if (isDefined(matchByLinkedin)) {
return matchByLinkedin;
}
}
if (isNonEmptyString(name)) {
const matchByName = await findCompanyIdByFilter(client, {
name: { eq: name },
});
if (isDefined(matchByName)) {
return matchByName;
}
}
return undefined;
};
@@ -0,0 +1,52 @@
import { isNonEmptyString } from '@sniptt/guards';
import { type CoreApiClient } from 'twenty-client-sdk/core';
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 PdlPersonData } from 'src/types/pdl-person-data.type';
import { isDefined } from 'src/utils/is-defined';
import { isUniqueViolationError } from 'src/utils/is-unique-violation-error';
type CreateCompanyResult = { createCompany?: { id: string } };
export const findOrCreateCurrentCompany = async (
client: CoreApiClient,
data: PdlPersonData,
): Promise<string | undefined> => {
const matchKeys = buildCompanyMatchKeys(data);
if (
!isNonEmptyString(matchKeys.name) &&
!isNonEmptyString(matchKeys.website)
) {
return undefined;
}
const existingId = await findCompanyId(client, matchKeys);
if (isDefined(existingId)) {
return existingId;
}
try {
const result = (await client.mutation({
createCompany: {
__args: { data: buildCompanyCreateData(data) },
id: true,
},
})) as CreateCompanyResult;
return result.createCompany?.id;
} catch (createError) {
if (!isUniqueViolationError(createError)) {
throw createError;
}
const raceWinnerId = await findCompanyId(client, matchKeys);
if (isDefined(raceWinnerId)) {
return raceWinnerId;
}
throw createError;
}
};
@@ -8,7 +8,6 @@ import { JOB_TITLE_SUB_ROLE_OPTIONS } from 'src/constants/job-title-sub-role-opt
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 { SIZE_OPTIONS } from 'src/constants/size-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';
@@ -23,7 +22,6 @@ import { buildAllowedValues } from 'src/logic-functions/utils/build-allowed-valu
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 { sizeTransform } from 'src/logic-functions/utils/size-transform';
import { type MappedRecord } from 'src/types/mapped-record.type';
import { type PdlPersonData } from 'src/types/pdl-person-data.type';
import { pruneUndefined } from 'src/utils/prune-undefined';
@@ -36,7 +34,6 @@ 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);
const SIZE_VALUES = buildAllowedValues(SIZE_OPTIONS);
export const mapPerson = (data: PdlPersonData): MappedRecord => {
const emailEntries = (data.emails ?? []).map((entry) =>
@@ -111,17 +108,6 @@ export const mapPerson = (data: PdlPersonData): MappedRecord => {
country: data.location_country,
geo: data.location_geo,
}),
pdlJobCompanyId: toText(data.job_company_id),
pdlJobCompanyName: toText(data.job_company_name),
pdlJobCompanyWebsite: buildLinks(data.job_company_website),
pdlJobCompanyLinkedinUrl: buildLinks(data.job_company_linkedin_url),
pdlJobCompanyIndustry: pickSelect(data.job_company_industry, INDUSTRY_VALUES),
pdlJobCompanySize: pickSelect(
data.job_company_size,
SIZE_VALUES,
sizeTransform,
),
});
return { standard, pdl };
@@ -17,6 +17,7 @@ export const readPerson = async (
phones: { primaryPhoneNumber: true },
jobTitle: true,
linkedinLink: { primaryLinkUrl: true },
company: { id: true },
pdlId: true,
pdlLastEnrichedAt: true,
},
@@ -0,0 +1,6 @@
export type CompanyMatchKeys = {
pdlId?: string;
website?: string;
linkedinUrl?: string;
name?: string;
};
@@ -5,6 +5,7 @@ export type PersonNode = {
phones?: { primaryPhoneNumber?: string | null } | null;
jobTitle?: string | null;
linkedinLink?: { primaryLinkUrl?: string | null } | null;
company?: { id?: string | null } | null;
pdlId?: string | null;
pdlLastEnrichedAt?: string | null;
};
@@ -0,0 +1,27 @@
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);
});
});
@@ -0,0 +1,19 @@
export const isUniqueViolationError = (error: unknown): boolean => {
const text =
error instanceof Error
? error.message
: typeof error === 'object' && error !== null
? JSON.stringify(error)
: typeof error === 'string'
? error
: '';
const lower = text.toLowerCase();
return (
lower.includes('duplicate') ||
lower.includes('unique constraint') ||
lower.includes('uniqueness') ||
lower.includes('already exists') ||
lower.includes('violates unique')
);
};
@@ -29,6 +29,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/aix-ppc64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/aix-ppc64@npm:0.28.0"
conditions: os=aix & cpu=ppc64
languageName: node
linkType: hard
"@esbuild/android-arm64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/android-arm64@npm:0.25.12"
@@ -43,6 +50,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/android-arm64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/android-arm64@npm:0.28.0"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@esbuild/android-arm@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/android-arm@npm:0.25.12"
@@ -57,6 +71,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/android-arm@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/android-arm@npm:0.28.0"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@esbuild/android-x64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/android-x64@npm:0.25.12"
@@ -71,6 +92,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/android-x64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/android-x64@npm:0.28.0"
conditions: os=android & cpu=x64
languageName: node
linkType: hard
"@esbuild/darwin-arm64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/darwin-arm64@npm:0.25.12"
@@ -85,6 +113,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/darwin-arm64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/darwin-arm64@npm:0.28.0"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@esbuild/darwin-x64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/darwin-x64@npm:0.25.12"
@@ -99,6 +134,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/darwin-x64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/darwin-x64@npm:0.28.0"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@esbuild/freebsd-arm64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/freebsd-arm64@npm:0.25.12"
@@ -113,6 +155,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/freebsd-arm64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/freebsd-arm64@npm:0.28.0"
conditions: os=freebsd & cpu=arm64
languageName: node
linkType: hard
"@esbuild/freebsd-x64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/freebsd-x64@npm:0.25.12"
@@ -127,6 +176,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/freebsd-x64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/freebsd-x64@npm:0.28.0"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/linux-arm64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/linux-arm64@npm:0.25.12"
@@ -141,6 +197,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-arm64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/linux-arm64@npm:0.28.0"
conditions: os=linux & cpu=arm64
languageName: node
linkType: hard
"@esbuild/linux-arm@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/linux-arm@npm:0.25.12"
@@ -155,6 +218,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-arm@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/linux-arm@npm:0.28.0"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@esbuild/linux-ia32@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/linux-ia32@npm:0.25.12"
@@ -169,6 +239,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-ia32@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/linux-ia32@npm:0.28.0"
conditions: os=linux & cpu=ia32
languageName: node
linkType: hard
"@esbuild/linux-loong64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/linux-loong64@npm:0.25.12"
@@ -183,6 +260,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-loong64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/linux-loong64@npm:0.28.0"
conditions: os=linux & cpu=loong64
languageName: node
linkType: hard
"@esbuild/linux-mips64el@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/linux-mips64el@npm:0.25.12"
@@ -197,6 +281,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-mips64el@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/linux-mips64el@npm:0.28.0"
conditions: os=linux & cpu=mips64el
languageName: node
linkType: hard
"@esbuild/linux-ppc64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/linux-ppc64@npm:0.25.12"
@@ -211,6 +302,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-ppc64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/linux-ppc64@npm:0.28.0"
conditions: os=linux & cpu=ppc64
languageName: node
linkType: hard
"@esbuild/linux-riscv64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/linux-riscv64@npm:0.25.12"
@@ -225,6 +323,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-riscv64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/linux-riscv64@npm:0.28.0"
conditions: os=linux & cpu=riscv64
languageName: node
linkType: hard
"@esbuild/linux-s390x@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/linux-s390x@npm:0.25.12"
@@ -239,6 +344,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-s390x@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/linux-s390x@npm:0.28.0"
conditions: os=linux & cpu=s390x
languageName: node
linkType: hard
"@esbuild/linux-x64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/linux-x64@npm:0.25.12"
@@ -253,6 +365,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/linux-x64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/linux-x64@npm:0.28.0"
conditions: os=linux & cpu=x64
languageName: node
linkType: hard
"@esbuild/netbsd-arm64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/netbsd-arm64@npm:0.25.12"
@@ -267,6 +386,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/netbsd-arm64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/netbsd-arm64@npm:0.28.0"
conditions: os=netbsd & cpu=arm64
languageName: node
linkType: hard
"@esbuild/netbsd-x64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/netbsd-x64@npm:0.25.12"
@@ -281,6 +407,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/netbsd-x64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/netbsd-x64@npm:0.28.0"
conditions: os=netbsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/openbsd-arm64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/openbsd-arm64@npm:0.25.12"
@@ -295,6 +428,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/openbsd-arm64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/openbsd-arm64@npm:0.28.0"
conditions: os=openbsd & cpu=arm64
languageName: node
linkType: hard
"@esbuild/openbsd-x64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/openbsd-x64@npm:0.25.12"
@@ -309,6 +449,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/openbsd-x64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/openbsd-x64@npm:0.28.0"
conditions: os=openbsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/openharmony-arm64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/openharmony-arm64@npm:0.25.12"
@@ -323,6 +470,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/openharmony-arm64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/openharmony-arm64@npm:0.28.0"
conditions: os=openharmony & cpu=arm64
languageName: node
linkType: hard
"@esbuild/sunos-x64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/sunos-x64@npm:0.25.12"
@@ -337,6 +491,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/sunos-x64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/sunos-x64@npm:0.28.0"
conditions: os=sunos & cpu=x64
languageName: node
linkType: hard
"@esbuild/win32-arm64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/win32-arm64@npm:0.25.12"
@@ -351,6 +512,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/win32-arm64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/win32-arm64@npm:0.28.0"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@esbuild/win32-ia32@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/win32-ia32@npm:0.25.12"
@@ -365,6 +533,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/win32-ia32@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/win32-ia32@npm:0.28.0"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@esbuild/win32-x64@npm:0.25.12":
version: 0.25.12
resolution: "@esbuild/win32-x64@npm:0.25.12"
@@ -379,6 +554,13 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/win32-x64@npm:0.28.0":
version: 0.28.0
resolution: "@esbuild/win32-x64@npm:0.28.0"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@fastify/busboy@npm:^2.0.0":
version: 2.1.1
resolution: "@fastify/busboy@npm:2.1.1"
@@ -1678,13 +1860,6 @@ __metadata:
languageName: node
linkType: hard
"dotenv@npm:^16.4.0":
version: 16.6.1
resolution: "dotenv@npm:16.6.1"
checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc
languageName: node
linkType: hard
"dunder-proto@npm:^1.0.1":
version: 1.0.1
resolution: "dunder-proto@npm:1.0.1"
@@ -1965,6 +2140,95 @@ __metadata:
languageName: node
linkType: hard
"esbuild@npm:^0.28.0":
version: 0.28.0
resolution: "esbuild@npm:0.28.0"
dependencies:
"@esbuild/aix-ppc64": "npm:0.28.0"
"@esbuild/android-arm": "npm:0.28.0"
"@esbuild/android-arm64": "npm:0.28.0"
"@esbuild/android-x64": "npm:0.28.0"
"@esbuild/darwin-arm64": "npm:0.28.0"
"@esbuild/darwin-x64": "npm:0.28.0"
"@esbuild/freebsd-arm64": "npm:0.28.0"
"@esbuild/freebsd-x64": "npm:0.28.0"
"@esbuild/linux-arm": "npm:0.28.0"
"@esbuild/linux-arm64": "npm:0.28.0"
"@esbuild/linux-ia32": "npm:0.28.0"
"@esbuild/linux-loong64": "npm:0.28.0"
"@esbuild/linux-mips64el": "npm:0.28.0"
"@esbuild/linux-ppc64": "npm:0.28.0"
"@esbuild/linux-riscv64": "npm:0.28.0"
"@esbuild/linux-s390x": "npm:0.28.0"
"@esbuild/linux-x64": "npm:0.28.0"
"@esbuild/netbsd-arm64": "npm:0.28.0"
"@esbuild/netbsd-x64": "npm:0.28.0"
"@esbuild/openbsd-arm64": "npm:0.28.0"
"@esbuild/openbsd-x64": "npm:0.28.0"
"@esbuild/openharmony-arm64": "npm:0.28.0"
"@esbuild/sunos-x64": "npm:0.28.0"
"@esbuild/win32-arm64": "npm:0.28.0"
"@esbuild/win32-ia32": "npm:0.28.0"
"@esbuild/win32-x64": "npm:0.28.0"
dependenciesMeta:
"@esbuild/aix-ppc64":
optional: true
"@esbuild/android-arm":
optional: true
"@esbuild/android-arm64":
optional: true
"@esbuild/android-x64":
optional: true
"@esbuild/darwin-arm64":
optional: true
"@esbuild/darwin-x64":
optional: true
"@esbuild/freebsd-arm64":
optional: true
"@esbuild/freebsd-x64":
optional: true
"@esbuild/linux-arm":
optional: true
"@esbuild/linux-arm64":
optional: true
"@esbuild/linux-ia32":
optional: true
"@esbuild/linux-loong64":
optional: true
"@esbuild/linux-mips64el":
optional: true
"@esbuild/linux-ppc64":
optional: true
"@esbuild/linux-riscv64":
optional: true
"@esbuild/linux-s390x":
optional: true
"@esbuild/linux-x64":
optional: true
"@esbuild/netbsd-arm64":
optional: true
"@esbuild/netbsd-x64":
optional: true
"@esbuild/openbsd-arm64":
optional: true
"@esbuild/openbsd-x64":
optional: true
"@esbuild/openharmony-arm64":
optional: true
"@esbuild/sunos-x64":
optional: true
"@esbuild/win32-arm64":
optional: true
"@esbuild/win32-ia32":
optional: true
"@esbuild/win32-x64":
optional: true
bin:
esbuild: bin/esbuild
checksum: 10c0/8acd95c238ec6c4a9d16163277faf228a8994b642d187b3fe667ffbb469008e6748cde144fdc3c175bf8e78ee49e15a0ed9b9f183fdb5fcea1772f87fb1372a4
languageName: node
linkType: hard
"escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5":
version: 1.0.5
resolution: "escape-string-regexp@npm:1.0.5"
@@ -3065,8 +3329,8 @@ __metadata:
oxlint: "npm:^0.16.0"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
twenty-client-sdk: "npm:2.4.0"
twenty-sdk: "npm:2.4.0"
twenty-client-sdk: "npm:2.9.1"
twenty-sdk: "npm:2.9.1"
typescript: "npm:^5.9.3"
vite-tsconfig-paths: "npm:^4.2.1"
vitest: "npm:^3.1.1"
@@ -3791,30 +4055,28 @@ __metadata:
languageName: node
linkType: hard
"twenty-client-sdk@npm:2.4.0":
version: 2.4.0
resolution: "twenty-client-sdk@npm:2.4.0"
"twenty-client-sdk@npm:2.9.1":
version: 2.9.1
resolution: "twenty-client-sdk@npm:2.9.1"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
esbuild: "npm:^0.25.0"
esbuild: "npm:^0.28.0"
graphql: "npm:^16.8.1"
checksum: 10c0/cdd9e70a59b8ce16f40e84b79c16e89b984dfcbb04e4646341fea40911acee8eab5aa61872421f25212949e677a75ed51af99e61829f2902af0f0a10e6e5614d
checksum: 10c0/fecc82f090f7f5261330a552017e31decf9f4e134e9cf54d8631666d47f303e5d1d229436f30d1ebd9983613eeba1adb8f7244741b88234e3e8cfe956f3a8ffb
languageName: node
linkType: hard
"twenty-sdk@npm:2.4.0":
version: 2.4.0
resolution: "twenty-sdk@npm:2.4.0"
"twenty-sdk@npm:2.9.1":
version: 2.9.1
resolution: "twenty-sdk@npm:2.9.1"
dependencies:
"@genql/cli": "npm:^3.0.3"
"@genql/runtime": "npm:^2.10.0"
"@sniptt/guards": "npm:^0.2.0"
axios: "npm:^1.13.5"
chalk: "npm:^5.3.0"
chokidar: "npm:^4.0.0"
commander: "npm:^12.0.0"
dotenv: "npm:^16.4.0"
esbuild: "npm:^0.25.0"
graphql: "npm:^16.8.1"
graphql-sse: "npm:^2.5.4"
@@ -3825,15 +4087,12 @@ __metadata:
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
tinyglobby: "npm:^0.2.15"
twenty-client-sdk: "npm:2.4.0"
typescript: "npm:^5.9.2"
twenty-client-sdk: "npm:2.9.1"
typescript: "npm:^5.9.3"
uuid: "npm:^13.0.0"
vite: "npm:^7.0.0"
vite-tsconfig-paths: "npm:^4.2.1"
zod: "npm:^4.1.11"
bin:
twenty: dist/cli.cjs
checksum: 10c0/7266755dd0f192f0107b25b72db7340a8476f4b97f3e8d2fa539010f6702f9104463da8ddca9318b2e7df2d6be43362be5eddb95af2067d8863578907b713617
checksum: 10c0/3bb4cd1cee344569a5a1c5f0f3af5afc1c4f84587aec983645c0d113dcb594b4d0723679b6181afbd389e83e84572e24e5fba60d6e0a9b062271dce0aeb9bbb4
languageName: node
linkType: hard
@@ -3853,7 +4112,7 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:^5.9.2, typescript@npm:^5.9.3":
"typescript@npm:^5.9.3":
version: 5.9.3
resolution: "typescript@npm:5.9.3"
bin:
@@ -3863,7 +4122,7 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.9.2#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.9.3#optional!builtin<compat/typescript>":
"typescript@patch:typescript@npm%3A^5.9.3#optional!builtin<compat/typescript>":
version: 5.9.3
resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"
bin:
@@ -3987,7 +4246,7 @@ __metadata:
languageName: node
linkType: hard
"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0, vite@npm:^7.0.0":
"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0":
version: 7.3.3
resolution: "vite@npm:7.3.3"
dependencies:
@@ -4312,10 +4571,3 @@ __metadata:
checksum: 10c0/71cc2f2bbb537300c3f569e25693d37b3bc91f225cefce251a71c30bc6bb3e7f8e9420ca0eb57f2ac9e492b085b8dfa075fd1e8195c40b83c951dd59c6e4fbf8
languageName: node
linkType: hard
"zod@npm:^4.1.11":
version: 4.4.3
resolution: "zod@npm:4.4.3"
checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3
languageName: node
linkType: hard