@dench.com/cli 3.6.3-staging.372 → 3.6.4-staging.377
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/crm.ts +62 -16
- package/dench.ts +6 -5
- package/lib/enrichment-gateway.ts +12 -182
- package/lib/enrichment-shapes.ts +182 -0
- package/package.json +1 -1
package/crm.ts
CHANGED
|
@@ -450,6 +450,13 @@ function parseCreateManyRows(items: unknown[]): JsonRecord[] {
|
|
|
450
450
|
);
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
function parseDedupeFieldFlag(raw: string | undefined): string | undefined {
|
|
454
|
+
if (raw !== undefined && !raw.trim()) {
|
|
455
|
+
throw new CrmCliError("--dedupe-key must name a non-empty CRM field");
|
|
456
|
+
}
|
|
457
|
+
return raw;
|
|
458
|
+
}
|
|
459
|
+
|
|
453
460
|
/**
|
|
454
461
|
* Stable per-row dedupe key derived from a named column. Values are
|
|
455
462
|
* normalized (String → trim → lowercase) so `" Jane@X.com"` and
|
|
@@ -464,6 +471,16 @@ function dedupeKeyFromColumn(opts: {
|
|
|
464
471
|
command: string;
|
|
465
472
|
}): string {
|
|
466
473
|
const raw = opts.row[opts.column];
|
|
474
|
+
if (
|
|
475
|
+
raw !== null &&
|
|
476
|
+
raw !== undefined &&
|
|
477
|
+
typeof raw !== "string" &&
|
|
478
|
+
(typeof raw !== "number" || !Number.isFinite(raw))
|
|
479
|
+
) {
|
|
480
|
+
throw new CrmCliError(
|
|
481
|
+
`${opts.command}: row ${opts.rowIndex + 1} must have a string or finite number for --dedupe-key column "${opts.column}"`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
467
484
|
const normalized =
|
|
468
485
|
raw === null || raw === undefined ? "" : String(raw).trim().toLowerCase();
|
|
469
486
|
if (!normalized) {
|
|
@@ -571,6 +588,8 @@ export type CrmBatchOp =
|
|
|
571
588
|
data: JsonRecord;
|
|
572
589
|
/** Stable per-row dedupe key (server upserts on conflict). */
|
|
573
590
|
seedKey?: string;
|
|
591
|
+
/** Match existing CRM entries by this scalar field before writing. */
|
|
592
|
+
matchField?: string;
|
|
574
593
|
/** 1-based source row for server-side error messages. */
|
|
575
594
|
rowIndex?: number;
|
|
576
595
|
}
|
|
@@ -2638,10 +2657,18 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
2638
2657
|
const objectName = shift(ctx.args, "object name");
|
|
2639
2658
|
const dataRaw = getDataFlag(ctx.args);
|
|
2640
2659
|
const idempotencyKey = getFlag(ctx.args, "--idempotency-key");
|
|
2641
|
-
//
|
|
2642
|
-
//
|
|
2643
|
-
|
|
2644
|
-
|
|
2660
|
+
// Match ordinary existing records by this field, retaining the
|
|
2661
|
+
// stable seedKey for retry compatibility with earlier CLI versions.
|
|
2662
|
+
const dedupeColumn = parseDedupeFieldFlag(
|
|
2663
|
+
getFlag(ctx.args, "--dedupe-key"),
|
|
2664
|
+
);
|
|
2665
|
+
const onConflictFlag = getFlag(ctx.args, "--on-conflict");
|
|
2666
|
+
const onConflict = parseOnConflictFlag(onConflictFlag);
|
|
2667
|
+
if (onConflictFlag !== undefined && !dedupeColumn) {
|
|
2668
|
+
throw new CrmCliError(
|
|
2669
|
+
"crm entries create-many: --on-conflict requires --dedupe-key <field>",
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2645
2672
|
const allowUnknown = hasFlag(ctx.args, "--allow-unknown");
|
|
2646
2673
|
const rows = parseCreateManyRows(
|
|
2647
2674
|
await readJsonArrayInput({
|
|
@@ -2670,12 +2697,16 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
2670
2697
|
objectName,
|
|
2671
2698
|
data,
|
|
2672
2699
|
...(seedKeys
|
|
2673
|
-
? {
|
|
2700
|
+
? {
|
|
2701
|
+
seedKey: seedKeys[index],
|
|
2702
|
+
matchField: dedupeColumn,
|
|
2703
|
+
rowIndex: index + 1,
|
|
2704
|
+
}
|
|
2674
2705
|
: {}),
|
|
2675
2706
|
})),
|
|
2676
2707
|
idempotencyKey,
|
|
2677
2708
|
allowUnknown,
|
|
2678
|
-
seedKeys ?
|
|
2709
|
+
seedKeys ? onConflict : undefined,
|
|
2679
2710
|
);
|
|
2680
2711
|
// Only claim per-row links when we actually got a result per row. A
|
|
2681
2712
|
// silently-empty `results` is indistinguishable from "this object has
|
|
@@ -2687,7 +2718,10 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
2687
2718
|
out(ctx, {
|
|
2688
2719
|
created: statusCounts ? statusCounts.created : rows.length,
|
|
2689
2720
|
...(statusCounts
|
|
2690
|
-
? {
|
|
2721
|
+
? {
|
|
2722
|
+
alreadyExisted: statusCounts.alreadyExisted,
|
|
2723
|
+
updated: statusCounts.updated,
|
|
2724
|
+
}
|
|
2691
2725
|
: {}),
|
|
2692
2726
|
chunks,
|
|
2693
2727
|
...(linked > 0
|
|
@@ -2788,6 +2822,11 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
2788
2822
|
const _objectName = shift(ctx.args, "object name");
|
|
2789
2823
|
const entryId = shift(ctx.args, "entry id");
|
|
2790
2824
|
assertNoUnknownFlags(ctx.args, "crm entries delete");
|
|
2825
|
+
if (ctx.args.length > 0) {
|
|
2826
|
+
throw new CrmCliError(
|
|
2827
|
+
"crm entries delete accepts exactly one entry ID. For multiple entries, use dench crm entries bulk-delete <object> --ids id1,id2,... or crm batch with entries.delete operations.",
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2791
2830
|
out(
|
|
2792
2831
|
ctx,
|
|
2793
2832
|
await callMutation(ctx, api.entries.remove, {
|
|
@@ -3519,7 +3558,7 @@ async function runImportCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
3519
3558
|
const csvPath = getFlag(ctx.args, "--csv");
|
|
3520
3559
|
const jsonlPath = getFlag(ctx.args, "--jsonl");
|
|
3521
3560
|
const mapJson = getFlag(ctx.args, "--map");
|
|
3522
|
-
const dedupeColumn = getFlag(ctx.args, "--dedupe-key");
|
|
3561
|
+
const dedupeColumn = parseDedupeFieldFlag(getFlag(ctx.args, "--dedupe-key"));
|
|
3523
3562
|
const onConflict = parseOnConflictFlag(getFlag(ctx.args, "--on-conflict"));
|
|
3524
3563
|
assertNoUnknownFlags(ctx.args, "crm import");
|
|
3525
3564
|
if (!csvPath && !jsonlPath) {
|
|
@@ -3557,10 +3596,11 @@ async function runImportCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
3557
3596
|
dedupeColumn,
|
|
3558
3597
|
sha256,
|
|
3559
3598
|
}),
|
|
3599
|
+
...(dedupeColumn ? { matchField: dedupeColumn } : {}),
|
|
3560
3600
|
rowIndex: index + 1,
|
|
3561
3601
|
}));
|
|
3562
|
-
// batch.apply
|
|
3563
|
-
//
|
|
3602
|
+
// batch.apply matches the selected CRM field when supplied; otherwise
|
|
3603
|
+
// seedKey protects exact reruns. The shared helper handles chunking.
|
|
3564
3604
|
const { results } = await applyBatchOpsInChunks(
|
|
3565
3605
|
ctx,
|
|
3566
3606
|
ops,
|
|
@@ -4840,9 +4880,10 @@ Tasks (shortcuts over the protected 'task' object):
|
|
|
4840
4880
|
dench crm entries create <object> --data '{"Field":"Value",...}' (alias: --fields) [--allow-unknown] [--idempotency-key <key>]
|
|
4841
4881
|
--idempotency-key upserts on (object, key): a retried create returns
|
|
4842
4882
|
the existing row with alreadyExisted: true instead of duplicating it.
|
|
4843
|
-
dench crm entries create-many <object> --data '[{"Field":"Value",...}]' [--file rows.json] [--idempotency-key <key>] [--dedupe-key <
|
|
4844
|
-
--dedupe-key <
|
|
4845
|
-
|
|
4883
|
+
dench crm entries create-many <object> --data '[{"Field":"Value",...}]' [--file rows.json] [--idempotency-key <key>] [--dedupe-key <field>] [--on-conflict skip|update|error] [--allow-unknown]
|
|
4884
|
+
--dedupe-key <field> matches existing CRM rows by field value,
|
|
4885
|
+
including rows created outside imports. --on-conflict requires it;
|
|
4886
|
+
default skip leaves matches untouched, update merges, error aborts.
|
|
4846
4887
|
dench crm entries update <object> <entryId> --data '{...}' (alias: --fields) [--allow-unknown]
|
|
4847
4888
|
dench crm entries update-many <object> --updates '[{"id":"...","data":{...}}]' [--file updates.json] [--idempotency-key <key>] [--allow-unknown]
|
|
4848
4889
|
dench crm entries delete <object> <entryId>
|
|
@@ -4934,9 +4975,14 @@ Batch:
|
|
|
4934
4975
|
Import / export:
|
|
4935
4976
|
dench crm import <object> --csv leads.csv [--map '{"Email":"Email Address"}'] [--on-conflict skip|update|error] [--dedupe-key <column>]
|
|
4936
4977
|
dench crm import <object> --jsonl rows.jsonl
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4978
|
+
--dedupe-key names the CRM field AFTER --map. Values match existing
|
|
4979
|
+
rows after trim + lowercase; supported fields are text, email, phone,
|
|
4980
|
+
URL, single-select enum, or number. Blank/non-scalar values fail.
|
|
4981
|
+
Number fields compare numeric values ("01" matches 1).
|
|
4982
|
+
Multiple matches or an incomplete bounded lookup abort the current
|
|
4983
|
+
batch without writes; earlier batches may already have committed.
|
|
4984
|
+
Without --dedupe-key, a content-hash key protects exact reimports
|
|
4985
|
+
only; it does not match ordinary existing records by field value.
|
|
4940
4986
|
dench crm export <object> --format csv|jsonl [--limit N]
|
|
4941
4987
|
Pages with a real cursor and verifies no duplicate ids before
|
|
4942
4988
|
emitting; a summary (exportedCount/uniqueIds) is printed to stderr.
|
package/dench.ts
CHANGED
|
@@ -3550,7 +3550,8 @@ async function runFilesLs() {
|
|
|
3550
3550
|
const rows = recursive
|
|
3551
3551
|
? await listConvexTreeRecursive(runtime, apiKey, target)
|
|
3552
3552
|
: await listConvexTreeAt(runtime, apiKey, target);
|
|
3553
|
-
|
|
3553
|
+
// Inline handlers share the top-level flag; filteredArgs omits --json.
|
|
3554
|
+
if (json) {
|
|
3554
3555
|
print({
|
|
3555
3556
|
ok: true,
|
|
3556
3557
|
prefix: target,
|
|
@@ -3670,7 +3671,7 @@ async function runFilesRm() {
|
|
|
3670
3671
|
);
|
|
3671
3672
|
const self = parentRows.find((row) => row.path === target);
|
|
3672
3673
|
if (!self) {
|
|
3673
|
-
if (
|
|
3674
|
+
if (json) {
|
|
3674
3675
|
print({ ok: true, path: target, removedFiles: 0, removedDirs: 0 });
|
|
3675
3676
|
} else {
|
|
3676
3677
|
process.stdout.write(
|
|
@@ -3772,7 +3773,7 @@ async function runFilesCat() {
|
|
|
3772
3773
|
});
|
|
3773
3774
|
}
|
|
3774
3775
|
|
|
3775
|
-
if (
|
|
3776
|
+
if (json) {
|
|
3776
3777
|
print({ ok: true, ...file });
|
|
3777
3778
|
return;
|
|
3778
3779
|
}
|
|
@@ -3822,7 +3823,7 @@ async function runFilesWrite() {
|
|
|
3822
3823
|
} as never,
|
|
3823
3824
|
)) as { path: string; contentHash: string };
|
|
3824
3825
|
|
|
3825
|
-
if (
|
|
3826
|
+
if (json) {
|
|
3826
3827
|
print({ ok: true, ...result, bytes: content.length });
|
|
3827
3828
|
return;
|
|
3828
3829
|
}
|
|
@@ -3850,7 +3851,7 @@ async function runFilesDownload() {
|
|
|
3850
3851
|
);
|
|
3851
3852
|
}
|
|
3852
3853
|
const target = normalizeFilesPath(targetRaw);
|
|
3853
|
-
const jsonOut =
|
|
3854
|
+
const jsonOut = json;
|
|
3854
3855
|
|
|
3855
3856
|
if (hasFlag("--url")) {
|
|
3856
3857
|
const url = (await runtime.client.action(
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EnrichedCompanyUpsertLike,
|
|
3
|
+
type EnrichedPersonUpsertLike,
|
|
4
|
+
shapeCompanyToApolloEnvelope,
|
|
5
|
+
shapePersonToApolloEnvelope,
|
|
6
|
+
} from "./enrichment-shapes";
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
ApolloCompanyEnvelope,
|
|
10
|
+
ApolloPersonEnvelope,
|
|
11
|
+
} from "./enrichment-shapes";
|
|
12
|
+
|
|
1
13
|
import { extractDomain } from "./crm-enrichment";
|
|
2
14
|
|
|
3
15
|
export type FetchLike = (
|
|
@@ -315,188 +327,6 @@ function mapRequiredFieldsToEnrichFields(rf?: string[]): EnrichField[] {
|
|
|
315
327
|
// Apollo-shape envelope (preserves crm-enrichment catalog extraction paths)
|
|
316
328
|
// ---------------------------------------------------------------------------
|
|
317
329
|
|
|
318
|
-
export type ApolloPersonEnvelope = {
|
|
319
|
-
person: {
|
|
320
|
-
search_match?: {
|
|
321
|
-
derivedFields: string[];
|
|
322
|
-
basis: "current_title" | "employment_title";
|
|
323
|
-
};
|
|
324
|
-
name?: string;
|
|
325
|
-
first_name?: string;
|
|
326
|
-
last_name?: string;
|
|
327
|
-
email?: string;
|
|
328
|
-
work_email?: string;
|
|
329
|
-
personal_email?: string;
|
|
330
|
-
photo_url?: string;
|
|
331
|
-
linkedin_url?: string;
|
|
332
|
-
title?: string;
|
|
333
|
-
headline?: string;
|
|
334
|
-
location?: string;
|
|
335
|
-
organization?: {
|
|
336
|
-
name?: string;
|
|
337
|
-
domain?: string;
|
|
338
|
-
website_url?: string;
|
|
339
|
-
};
|
|
340
|
-
organization_name?: string;
|
|
341
|
-
contact: {
|
|
342
|
-
phone_numbers: Array<{
|
|
343
|
-
sanitized_number: string;
|
|
344
|
-
raw_number?: string;
|
|
345
|
-
}>;
|
|
346
|
-
};
|
|
347
|
-
};
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
type EnrichedPersonUpsertLike = {
|
|
351
|
-
avatarUrl?: string;
|
|
352
|
-
searchMatch?: {
|
|
353
|
-
derivedFields: string[];
|
|
354
|
-
basis: "current_title" | "employment_title";
|
|
355
|
-
};
|
|
356
|
-
fullName?: string;
|
|
357
|
-
firstName?: string;
|
|
358
|
-
lastName?: string;
|
|
359
|
-
headline?: string;
|
|
360
|
-
/** Structured title from the person's current role (distinct from headline). */
|
|
361
|
-
currentTitle?: string;
|
|
362
|
-
linkedinID?: string;
|
|
363
|
-
location?: string;
|
|
364
|
-
locationName?: string;
|
|
365
|
-
currentCompanyName?: string;
|
|
366
|
-
currentCompanyWebsite?: string;
|
|
367
|
-
emails?: Array<{
|
|
368
|
-
address?: string;
|
|
369
|
-
normalizedAddress?: string;
|
|
370
|
-
type?: string;
|
|
371
|
-
}>;
|
|
372
|
-
phones?: Array<{ number?: string; normalizedNumber?: string }>;
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
function shapePersonToApolloEnvelope(
|
|
376
|
-
u: EnrichedPersonUpsertLike,
|
|
377
|
-
): ApolloPersonEnvelope {
|
|
378
|
-
const workEmail = u.emails?.find((e) => e.type === "work")?.address;
|
|
379
|
-
const personalEmail = u.emails?.find((e) => e.type === "personal")?.address;
|
|
380
|
-
const anyEmail = u.emails?.[0]?.address;
|
|
381
|
-
const companyName = u.currentCompanyName;
|
|
382
|
-
const companyWebsite = u.currentCompanyWebsite;
|
|
383
|
-
return {
|
|
384
|
-
person: {
|
|
385
|
-
...(u.searchMatch ? { search_match: u.searchMatch } : {}),
|
|
386
|
-
name: u.fullName,
|
|
387
|
-
first_name: u.firstName,
|
|
388
|
-
last_name: u.lastName,
|
|
389
|
-
email: workEmail ?? anyEmail,
|
|
390
|
-
work_email: workEmail,
|
|
391
|
-
personal_email: personalEmail,
|
|
392
|
-
photo_url: u.avatarUrl,
|
|
393
|
-
linkedin_url: u.linkedinID
|
|
394
|
-
? `https://www.linkedin.com/in/${u.linkedinID}`
|
|
395
|
-
: undefined,
|
|
396
|
-
title: u.currentTitle,
|
|
397
|
-
headline: u.headline,
|
|
398
|
-
location: u.location ?? u.locationName,
|
|
399
|
-
organization:
|
|
400
|
-
companyName || companyWebsite
|
|
401
|
-
? {
|
|
402
|
-
name: companyName,
|
|
403
|
-
domain: companyWebsite,
|
|
404
|
-
website_url: companyWebsite,
|
|
405
|
-
}
|
|
406
|
-
: undefined,
|
|
407
|
-
organization_name: companyName,
|
|
408
|
-
contact: {
|
|
409
|
-
phone_numbers: (u.phones ?? []).map((p) => ({
|
|
410
|
-
sanitized_number: p.normalizedNumber ?? p.number ?? "",
|
|
411
|
-
raw_number: p.number,
|
|
412
|
-
})),
|
|
413
|
-
},
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ---------------------------------------------------------------------------
|
|
419
|
-
// Organization-shape envelope (preserves COMPANY_ENRICHMENT_COLUMNS paths)
|
|
420
|
-
// ---------------------------------------------------------------------------
|
|
421
|
-
|
|
422
|
-
export type ApolloCompanyEnvelope = {
|
|
423
|
-
organization: {
|
|
424
|
-
name?: string;
|
|
425
|
-
domain?: string;
|
|
426
|
-
website_url?: string;
|
|
427
|
-
industry?: string;
|
|
428
|
-
industry_list?: string[];
|
|
429
|
-
specialties?: string[];
|
|
430
|
-
linkedin_url?: string;
|
|
431
|
-
founded_year?: number;
|
|
432
|
-
headcount?: number;
|
|
433
|
-
headcount_range?: string;
|
|
434
|
-
total_funding?: number;
|
|
435
|
-
description?: string;
|
|
436
|
-
hq_city?: string;
|
|
437
|
-
hq_country?: string;
|
|
438
|
-
hq_country_code?: string;
|
|
439
|
-
hq_location?: string;
|
|
440
|
-
};
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
type EnrichedCompanyUpsertLike = {
|
|
444
|
-
name?: string;
|
|
445
|
-
website?: string;
|
|
446
|
-
industry?: string;
|
|
447
|
-
industryList?: string[];
|
|
448
|
-
specialties?: string[];
|
|
449
|
-
linkedinUrl?: string;
|
|
450
|
-
founded?: number;
|
|
451
|
-
headcount?: number;
|
|
452
|
-
headcountRange?: string;
|
|
453
|
-
totalFunding?: number;
|
|
454
|
-
description?: string;
|
|
455
|
-
hqAddress?: {
|
|
456
|
-
line1?: string;
|
|
457
|
-
line2?: string;
|
|
458
|
-
city?: string;
|
|
459
|
-
region?: string;
|
|
460
|
-
country?: string;
|
|
461
|
-
countryCode?: string;
|
|
462
|
-
};
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
function shapeCompanyToApolloEnvelope(
|
|
466
|
-
c: EnrichedCompanyUpsertLike,
|
|
467
|
-
): ApolloCompanyEnvelope {
|
|
468
|
-
const hq = c.hqAddress;
|
|
469
|
-
const city = hq?.city;
|
|
470
|
-
const country = hq?.country ?? hq?.countryCode;
|
|
471
|
-
const hqLocationParts = [city ?? hq?.line1, hq?.region, country].filter(
|
|
472
|
-
(v): v is string => typeof v === "string" && v.length > 0,
|
|
473
|
-
);
|
|
474
|
-
return {
|
|
475
|
-
organization: {
|
|
476
|
-
name: c.name,
|
|
477
|
-
domain: c.website,
|
|
478
|
-
website_url: c.website,
|
|
479
|
-
industry: c.industry,
|
|
480
|
-
industry_list: c.industryList,
|
|
481
|
-
specialties: c.specialties,
|
|
482
|
-
linkedin_url: c.linkedinUrl,
|
|
483
|
-
founded_year: c.founded,
|
|
484
|
-
headcount: c.headcount,
|
|
485
|
-
headcount_range: c.headcountRange,
|
|
486
|
-
total_funding: c.totalFunding,
|
|
487
|
-
description: c.description,
|
|
488
|
-
hq_city: city,
|
|
489
|
-
hq_country: hq?.country,
|
|
490
|
-
hq_country_code: hq?.countryCode,
|
|
491
|
-
hq_location:
|
|
492
|
-
hq?.line2?.trim() ||
|
|
493
|
-
(hqLocationParts.length > 0
|
|
494
|
-
? [...new Set(hqLocationParts)].join(", ")
|
|
495
|
-
: undefined),
|
|
496
|
-
},
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
330
|
// ---------------------------------------------------------------------------
|
|
501
331
|
// enrichPerson — calls POST /v1/enrichment/person/contact with block-and-poll
|
|
502
332
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/** Pure shared projection of gateway profiles into the existing entity-card contract. */
|
|
2
|
+
export type ApolloPersonEnvelope = {
|
|
3
|
+
person: {
|
|
4
|
+
search_match?: {
|
|
5
|
+
derivedFields: string[];
|
|
6
|
+
basis: "current_title" | "employment_title";
|
|
7
|
+
};
|
|
8
|
+
name?: string;
|
|
9
|
+
first_name?: string;
|
|
10
|
+
last_name?: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
work_email?: string;
|
|
13
|
+
personal_email?: string;
|
|
14
|
+
photo_url?: string;
|
|
15
|
+
linkedin_url?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
headline?: string;
|
|
18
|
+
location?: string;
|
|
19
|
+
organization?: {
|
|
20
|
+
name?: string;
|
|
21
|
+
domain?: string;
|
|
22
|
+
website_url?: string;
|
|
23
|
+
};
|
|
24
|
+
organization_name?: string;
|
|
25
|
+
contact: {
|
|
26
|
+
phone_numbers: Array<{
|
|
27
|
+
sanitized_number: string;
|
|
28
|
+
raw_number?: string;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type EnrichedPersonUpsertLike = {
|
|
35
|
+
avatarUrl?: string;
|
|
36
|
+
searchMatch?: {
|
|
37
|
+
derivedFields: string[];
|
|
38
|
+
basis: "current_title" | "employment_title";
|
|
39
|
+
};
|
|
40
|
+
fullName?: string;
|
|
41
|
+
firstName?: string;
|
|
42
|
+
lastName?: string;
|
|
43
|
+
headline?: string;
|
|
44
|
+
/** Structured title from the person's current role (distinct from headline). */
|
|
45
|
+
currentTitle?: string;
|
|
46
|
+
linkedinID?: string;
|
|
47
|
+
location?: string;
|
|
48
|
+
locationName?: string;
|
|
49
|
+
currentCompanyName?: string;
|
|
50
|
+
currentCompanyWebsite?: string;
|
|
51
|
+
emails?: Array<{
|
|
52
|
+
address?: string;
|
|
53
|
+
normalizedAddress?: string;
|
|
54
|
+
type?: string;
|
|
55
|
+
}>;
|
|
56
|
+
phones?: Array<{ number?: string; normalizedNumber?: string }>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function shapePersonToApolloEnvelope(
|
|
60
|
+
u: EnrichedPersonUpsertLike,
|
|
61
|
+
): ApolloPersonEnvelope {
|
|
62
|
+
const workEmail = u.emails?.find((e) => e.type === "work")?.address;
|
|
63
|
+
const personalEmail = u.emails?.find((e) => e.type === "personal")?.address;
|
|
64
|
+
const anyEmail = u.emails?.[0]?.address;
|
|
65
|
+
const companyName = u.currentCompanyName;
|
|
66
|
+
const companyWebsite = u.currentCompanyWebsite;
|
|
67
|
+
return {
|
|
68
|
+
person: {
|
|
69
|
+
...(u.searchMatch ? { search_match: u.searchMatch } : {}),
|
|
70
|
+
name: u.fullName,
|
|
71
|
+
first_name: u.firstName,
|
|
72
|
+
last_name: u.lastName,
|
|
73
|
+
email: workEmail ?? anyEmail,
|
|
74
|
+
work_email: workEmail,
|
|
75
|
+
personal_email: personalEmail,
|
|
76
|
+
photo_url: u.avatarUrl,
|
|
77
|
+
linkedin_url: u.linkedinID
|
|
78
|
+
? `https://www.linkedin.com/in/${u.linkedinID}`
|
|
79
|
+
: undefined,
|
|
80
|
+
title: u.currentTitle,
|
|
81
|
+
headline: u.headline,
|
|
82
|
+
location: u.location ?? u.locationName,
|
|
83
|
+
organization:
|
|
84
|
+
companyName || companyWebsite
|
|
85
|
+
? {
|
|
86
|
+
name: companyName,
|
|
87
|
+
domain: companyWebsite,
|
|
88
|
+
website_url: companyWebsite,
|
|
89
|
+
}
|
|
90
|
+
: undefined,
|
|
91
|
+
organization_name: companyName,
|
|
92
|
+
contact: {
|
|
93
|
+
phone_numbers: (u.phones ?? []).map((p) => ({
|
|
94
|
+
sanitized_number: p.normalizedNumber ?? p.number ?? "",
|
|
95
|
+
raw_number: p.number,
|
|
96
|
+
})),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Organization-shape envelope (preserves COMPANY_ENRICHMENT_COLUMNS paths)
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
export type ApolloCompanyEnvelope = {
|
|
107
|
+
organization: {
|
|
108
|
+
name?: string;
|
|
109
|
+
domain?: string;
|
|
110
|
+
website_url?: string;
|
|
111
|
+
industry?: string;
|
|
112
|
+
industry_list?: string[];
|
|
113
|
+
specialties?: string[];
|
|
114
|
+
linkedin_url?: string;
|
|
115
|
+
founded_year?: number;
|
|
116
|
+
headcount?: number;
|
|
117
|
+
headcount_range?: string;
|
|
118
|
+
total_funding?: number;
|
|
119
|
+
description?: string;
|
|
120
|
+
hq_city?: string;
|
|
121
|
+
hq_country?: string;
|
|
122
|
+
hq_country_code?: string;
|
|
123
|
+
hq_location?: string;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type EnrichedCompanyUpsertLike = {
|
|
128
|
+
name?: string;
|
|
129
|
+
website?: string;
|
|
130
|
+
industry?: string;
|
|
131
|
+
industryList?: string[];
|
|
132
|
+
specialties?: string[];
|
|
133
|
+
linkedinUrl?: string;
|
|
134
|
+
founded?: number;
|
|
135
|
+
headcount?: number;
|
|
136
|
+
headcountRange?: string;
|
|
137
|
+
totalFunding?: number;
|
|
138
|
+
description?: string;
|
|
139
|
+
hqAddress?: {
|
|
140
|
+
line1?: string;
|
|
141
|
+
line2?: string;
|
|
142
|
+
city?: string;
|
|
143
|
+
region?: string;
|
|
144
|
+
country?: string;
|
|
145
|
+
countryCode?: string;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export function shapeCompanyToApolloEnvelope(
|
|
150
|
+
c: EnrichedCompanyUpsertLike,
|
|
151
|
+
): ApolloCompanyEnvelope {
|
|
152
|
+
const hq = c.hqAddress;
|
|
153
|
+
const city = hq?.city;
|
|
154
|
+
const country = hq?.country ?? hq?.countryCode;
|
|
155
|
+
const hqLocationParts = [city ?? hq?.line1, hq?.region, country].filter(
|
|
156
|
+
(v): v is string => typeof v === "string" && v.length > 0,
|
|
157
|
+
);
|
|
158
|
+
return {
|
|
159
|
+
organization: {
|
|
160
|
+
name: c.name,
|
|
161
|
+
domain: c.website,
|
|
162
|
+
website_url: c.website,
|
|
163
|
+
industry: c.industry,
|
|
164
|
+
industry_list: c.industryList,
|
|
165
|
+
specialties: c.specialties,
|
|
166
|
+
linkedin_url: c.linkedinUrl,
|
|
167
|
+
founded_year: c.founded,
|
|
168
|
+
headcount: c.headcount,
|
|
169
|
+
headcount_range: c.headcountRange,
|
|
170
|
+
total_funding: c.totalFunding,
|
|
171
|
+
description: c.description,
|
|
172
|
+
hq_city: city,
|
|
173
|
+
hq_country: hq?.country,
|
|
174
|
+
hq_country_code: hq?.countryCode,
|
|
175
|
+
hq_location:
|
|
176
|
+
hq?.line2?.trim() ||
|
|
177
|
+
(hqLocationParts.length > 0
|
|
178
|
+
? [...new Set(hqLocationParts)].join(", ")
|
|
179
|
+
: undefined),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|