twenty-app-intake 0.4.0 → 0.5.0
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/README.md +93 -2
- package/manifest.json +46 -17
- package/package.json +1 -1
- package/src/logic-functions/contract.mjs +1 -1
- package/src/logic-functions/contract.mjs.map +2 -2
- package/src/logic-functions/health.mjs +1 -1
- package/src/logic-functions/health.mjs.map +1 -1
- package/src/logic-functions/quarantine-discard.mjs.map +2 -2
- package/src/logic-functions/quarantine-release.mjs +240 -26
- package/src/logic-functions/quarantine-release.mjs.map +3 -3
- package/src/logic-functions/replay-bulk.mjs +240 -26
- package/src/logic-functions/replay-bulk.mjs.map +3 -3
- package/src/logic-functions/replay-log.mjs +240 -26
- package/src/logic-functions/replay-log.mjs.map +3 -3
- package/src/logic-functions/retry.mjs +240 -26
- package/src/logic-functions/retry.mjs.map +3 -3
- package/src/logic-functions/test-ingest.mjs +254 -39
- package/src/logic-functions/test-ingest.mjs.map +3 -3
- package/src/logic-functions/test-ingest.ts +6 -2
- package/src/logic-functions/webhook.mjs +240 -26
- package/src/logic-functions/webhook.mjs.map +3 -3
- package/src/post-install.mjs +0 -7
- package/src/post-install.mjs.map +2 -2
- package/src/post-install.ts +8 -8
|
@@ -518,7 +518,7 @@ var FlattenStage = class {
|
|
|
518
518
|
|
|
519
519
|
// src/constants/field-map.ts
|
|
520
520
|
function normaliseKey(key) {
|
|
521
|
-
return key.replace(/([A-Z])/g, "
|
|
521
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase().replace(/[-\s.]/g, "_").replace(/_{2,}/g, "_");
|
|
522
522
|
}
|
|
523
523
|
var FIELD_MAP = {
|
|
524
524
|
// ── Phone ─────────────────────────────────────────────────────────────────
|
|
@@ -825,15 +825,56 @@ function parseTargetObject(raw) {
|
|
|
825
825
|
return "auto";
|
|
826
826
|
}
|
|
827
827
|
}
|
|
828
|
-
|
|
829
|
-
|
|
828
|
+
var PROTECTED_CANONICAL_NAMES = /* @__PURE__ */ new Set([
|
|
829
|
+
"email",
|
|
830
|
+
"phone",
|
|
831
|
+
"firstName",
|
|
832
|
+
"lastName",
|
|
833
|
+
"fullName"
|
|
834
|
+
]);
|
|
835
|
+
function compileRule(rule) {
|
|
836
|
+
try {
|
|
837
|
+
return { ...rule, matcher: new RegExp(rule.inputPattern, "i") };
|
|
838
|
+
} catch {
|
|
839
|
+
return { ...rule, matcher: null };
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function ruleMatches(rule, key, lookupKey) {
|
|
843
|
+
if (rule.inputPattern === key || rule.inputPattern === lookupKey) return true;
|
|
844
|
+
if (!rule.matcher) return false;
|
|
845
|
+
return rule.matcher.test(key) || rule.matcher.test(lookupKey);
|
|
846
|
+
}
|
|
847
|
+
function normalizeFields(flat, rules = [], warnings = []) {
|
|
848
|
+
const compiled = [...rules].sort((a, b) => b.priority - a.priority).map(compileRule);
|
|
830
849
|
return Object.entries(flat).map(
|
|
831
|
-
([key, value]) => normalizeField(key, value,
|
|
850
|
+
([key, value]) => normalizeField(key, value, compiled, warnings)
|
|
832
851
|
);
|
|
833
852
|
}
|
|
834
|
-
function normalizeField(key, value, rules) {
|
|
853
|
+
function normalizeField(key, value, rules, warnings) {
|
|
835
854
|
const lookupKey = normaliseKey(key);
|
|
836
855
|
const builtIn = FIELD_MAP[lookupKey];
|
|
856
|
+
for (const rule of rules) {
|
|
857
|
+
if (!ruleMatches(rule, key, lookupKey)) continue;
|
|
858
|
+
if (builtIn && PROTECTED_CANONICAL_NAMES.has(builtIn.canonicalName)) {
|
|
859
|
+
const displaces = rule.canonicalName !== builtIn.canonicalName || actionForRule(rule) !== "field";
|
|
860
|
+
if (displaces) {
|
|
861
|
+
warnings.push(
|
|
862
|
+
`Rule "${rule.inputPattern}" matched "${key}", which the built-in map holds as the contact's ${builtIn.canonicalName}. The built-in mapping was kept: a contact with no ${builtIn.canonicalName} cannot be found again, so every submission would create a duplicate. Point the rule at a key the built-in map does not already own.`
|
|
863
|
+
);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
originalKey: key,
|
|
869
|
+
canonicalName: rule.canonicalName,
|
|
870
|
+
twentyType: typeForRule(rule),
|
|
871
|
+
action: actionForRule(rule),
|
|
872
|
+
value,
|
|
873
|
+
source: "rule",
|
|
874
|
+
targetObject: parseTargetObject(rule.targetObject),
|
|
875
|
+
mergeStrategy: parseFieldMergeStrategy(rule.mergeStrategy)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
837
878
|
if (builtIn) {
|
|
838
879
|
return {
|
|
839
880
|
originalKey: key,
|
|
@@ -845,26 +886,6 @@ function normalizeField(key, value, rules) {
|
|
|
845
886
|
targetObject: "auto"
|
|
846
887
|
};
|
|
847
888
|
}
|
|
848
|
-
for (const rule of rules) {
|
|
849
|
-
let matches = false;
|
|
850
|
-
try {
|
|
851
|
-
matches = rule.inputPattern === key || rule.inputPattern === lookupKey || new RegExp(rule.inputPattern, "i").test(key);
|
|
852
|
-
} catch {
|
|
853
|
-
matches = rule.inputPattern === key;
|
|
854
|
-
}
|
|
855
|
-
if (matches) {
|
|
856
|
-
return {
|
|
857
|
-
originalKey: key,
|
|
858
|
-
canonicalName: rule.canonicalName,
|
|
859
|
-
twentyType: typeForRule(rule),
|
|
860
|
-
action: actionForRule(rule),
|
|
861
|
-
value,
|
|
862
|
-
source: "rule",
|
|
863
|
-
targetObject: parseTargetObject(rule.targetObject),
|
|
864
|
-
mergeStrategy: parseFieldMergeStrategy(rule.mergeStrategy)
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
889
|
const detectedType = detectType(value);
|
|
869
890
|
const classification = classify(key, value);
|
|
870
891
|
const prefix = OPPORTUNITY_KEY_PREFIXES.find((p) => lookupKey.startsWith(p));
|
|
@@ -979,7 +1000,7 @@ var NormalizeStage = class {
|
|
|
979
1000
|
return err(ErrorCode.INTERNAL_ERROR, "FlattenStage must run before NormalizeStage");
|
|
980
1001
|
}
|
|
981
1002
|
const rules = await this.loadRules(ctx.source?.id);
|
|
982
|
-
const normalized = normalizeFields(ctx.flat, rules);
|
|
1003
|
+
const normalized = normalizeFields(ctx.flat, rules, ctx.warnings);
|
|
983
1004
|
const { crmFields, noteFields } = partition(normalized);
|
|
984
1005
|
return ok({
|
|
985
1006
|
...ctx,
|
|
@@ -1443,6 +1464,183 @@ function formatValue(value) {
|
|
|
1443
1464
|
return String(value);
|
|
1444
1465
|
}
|
|
1445
1466
|
|
|
1467
|
+
// src/constants/free-email-domains.ts
|
|
1468
|
+
var FREE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
1469
|
+
// Google
|
|
1470
|
+
"gmail.com",
|
|
1471
|
+
"googlemail.com",
|
|
1472
|
+
// Microsoft
|
|
1473
|
+
"outlook.com",
|
|
1474
|
+
"outlook.co.uk",
|
|
1475
|
+
"outlook.fr",
|
|
1476
|
+
"outlook.de",
|
|
1477
|
+
"outlook.es",
|
|
1478
|
+
"hotmail.com",
|
|
1479
|
+
"hotmail.co.uk",
|
|
1480
|
+
"hotmail.fr",
|
|
1481
|
+
"hotmail.de",
|
|
1482
|
+
"hotmail.it",
|
|
1483
|
+
"hotmail.es",
|
|
1484
|
+
"live.com",
|
|
1485
|
+
"live.co.uk",
|
|
1486
|
+
"live.nl",
|
|
1487
|
+
"msn.com",
|
|
1488
|
+
"passport.com",
|
|
1489
|
+
// Yahoo
|
|
1490
|
+
"yahoo.com",
|
|
1491
|
+
"yahoo.co.uk",
|
|
1492
|
+
"yahoo.co.jp",
|
|
1493
|
+
"yahoo.ca",
|
|
1494
|
+
"yahoo.com.au",
|
|
1495
|
+
"yahoo.de",
|
|
1496
|
+
"yahoo.fr",
|
|
1497
|
+
"yahoo.es",
|
|
1498
|
+
"yahoo.it",
|
|
1499
|
+
"yahoo.in",
|
|
1500
|
+
"ymail.com",
|
|
1501
|
+
"rocketmail.com",
|
|
1502
|
+
// Apple
|
|
1503
|
+
"icloud.com",
|
|
1504
|
+
"me.com",
|
|
1505
|
+
"mac.com",
|
|
1506
|
+
// AOL / Verizon / legacy US ISPs — common on older consumer accounts
|
|
1507
|
+
"aol.com",
|
|
1508
|
+
"aim.com",
|
|
1509
|
+
"verizon.net",
|
|
1510
|
+
"att.net",
|
|
1511
|
+
"sbcglobal.net",
|
|
1512
|
+
"bellsouth.net",
|
|
1513
|
+
"comcast.net",
|
|
1514
|
+
"cox.net",
|
|
1515
|
+
"charter.net",
|
|
1516
|
+
"earthlink.net",
|
|
1517
|
+
"juno.com",
|
|
1518
|
+
"netzero.net",
|
|
1519
|
+
"roadrunner.com",
|
|
1520
|
+
"rr.com",
|
|
1521
|
+
"optonline.net",
|
|
1522
|
+
"frontier.com",
|
|
1523
|
+
"windstream.net",
|
|
1524
|
+
// Privacy-oriented providers
|
|
1525
|
+
"proton.me",
|
|
1526
|
+
"protonmail.com",
|
|
1527
|
+
"protonmail.ch",
|
|
1528
|
+
"pm.me",
|
|
1529
|
+
"tutanota.com",
|
|
1530
|
+
"tutanota.de",
|
|
1531
|
+
"tuta.io",
|
|
1532
|
+
"tuta.com",
|
|
1533
|
+
"hushmail.com",
|
|
1534
|
+
"fastmail.com",
|
|
1535
|
+
"fastmail.fm",
|
|
1536
|
+
"posteo.de",
|
|
1537
|
+
"mailbox.org",
|
|
1538
|
+
// Other large free providers
|
|
1539
|
+
"gmx.com",
|
|
1540
|
+
"gmx.net",
|
|
1541
|
+
"gmx.de",
|
|
1542
|
+
"gmx.at",
|
|
1543
|
+
"gmx.ch",
|
|
1544
|
+
"mail.com",
|
|
1545
|
+
"email.com",
|
|
1546
|
+
"usa.com",
|
|
1547
|
+
"inbox.com",
|
|
1548
|
+
"zoho.com",
|
|
1549
|
+
"zohomail.com",
|
|
1550
|
+
"yandex.com",
|
|
1551
|
+
"yandex.ru",
|
|
1552
|
+
"ya.ru",
|
|
1553
|
+
"mail.ru",
|
|
1554
|
+
"bk.ru",
|
|
1555
|
+
"list.ru",
|
|
1556
|
+
"inbox.ru",
|
|
1557
|
+
"rambler.ru",
|
|
1558
|
+
"qq.com",
|
|
1559
|
+
"foxmail.com",
|
|
1560
|
+
"163.com",
|
|
1561
|
+
"126.com",
|
|
1562
|
+
"sina.com",
|
|
1563
|
+
"sina.cn",
|
|
1564
|
+
"sohu.com",
|
|
1565
|
+
"naver.com",
|
|
1566
|
+
"hanmail.net",
|
|
1567
|
+
"daum.net",
|
|
1568
|
+
"nate.com",
|
|
1569
|
+
"seznam.cz",
|
|
1570
|
+
"wp.pl",
|
|
1571
|
+
"o2.pl",
|
|
1572
|
+
"onet.pl",
|
|
1573
|
+
"interia.pl",
|
|
1574
|
+
"libero.it",
|
|
1575
|
+
"virgilio.it",
|
|
1576
|
+
"alice.it",
|
|
1577
|
+
"tiscali.it",
|
|
1578
|
+
"orange.fr",
|
|
1579
|
+
"wanadoo.fr",
|
|
1580
|
+
"free.fr",
|
|
1581
|
+
"laposte.net",
|
|
1582
|
+
"sfr.fr",
|
|
1583
|
+
"bbox.fr",
|
|
1584
|
+
"neuf.fr",
|
|
1585
|
+
"web.de",
|
|
1586
|
+
"t-online.de",
|
|
1587
|
+
"freenet.de",
|
|
1588
|
+
"arcor.de",
|
|
1589
|
+
"btinternet.com",
|
|
1590
|
+
"sky.com",
|
|
1591
|
+
"talktalk.net",
|
|
1592
|
+
"virginmedia.com",
|
|
1593
|
+
"ntlworld.com",
|
|
1594
|
+
"bigpond.com",
|
|
1595
|
+
"bigpond.net.au",
|
|
1596
|
+
"optusnet.com.au",
|
|
1597
|
+
"iinet.net.au",
|
|
1598
|
+
"xtra.co.nz",
|
|
1599
|
+
"terra.com.br",
|
|
1600
|
+
"uol.com.br",
|
|
1601
|
+
"bol.com.br",
|
|
1602
|
+
"ig.com.br",
|
|
1603
|
+
"rediffmail.com",
|
|
1604
|
+
"sify.com",
|
|
1605
|
+
"indiatimes.com",
|
|
1606
|
+
// Throwaway / disposable services — never a real employer
|
|
1607
|
+
"mailinator.com",
|
|
1608
|
+
"guerrillamail.com",
|
|
1609
|
+
"guerrillamail.net",
|
|
1610
|
+
"sharklasers.com",
|
|
1611
|
+
"10minutemail.com",
|
|
1612
|
+
"temp-mail.org",
|
|
1613
|
+
"tempmail.com",
|
|
1614
|
+
"throwawaymail.com",
|
|
1615
|
+
"yopmail.com",
|
|
1616
|
+
"trashmail.com",
|
|
1617
|
+
"getnada.com",
|
|
1618
|
+
"dispostable.com",
|
|
1619
|
+
"maildrop.cc",
|
|
1620
|
+
"mintemail.com",
|
|
1621
|
+
"fakeinbox.com",
|
|
1622
|
+
"spamgourmet.com",
|
|
1623
|
+
"mailnesia.com",
|
|
1624
|
+
"moakt.com",
|
|
1625
|
+
"emailondeck.com",
|
|
1626
|
+
"burnermail.io",
|
|
1627
|
+
"anonaddy.com",
|
|
1628
|
+
"simplelogin.io",
|
|
1629
|
+
"duck.com",
|
|
1630
|
+
"relay.firefox.com",
|
|
1631
|
+
// Placeholder domains that show up in test and pasted payloads
|
|
1632
|
+
"example.com",
|
|
1633
|
+
"example.org",
|
|
1634
|
+
"example.net",
|
|
1635
|
+
"test.com",
|
|
1636
|
+
"localhost"
|
|
1637
|
+
]);
|
|
1638
|
+
function isFreeEmailDomain(host) {
|
|
1639
|
+
const normalised = host.trim().toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
|
|
1640
|
+
if (!normalised) return false;
|
|
1641
|
+
return FREE_EMAIL_DOMAINS.has(normalised);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1446
1644
|
// src/lib/lookup.ts
|
|
1447
1645
|
async function firstMatch(collection, query) {
|
|
1448
1646
|
try {
|
|
@@ -1460,11 +1658,16 @@ async function findCompanyByDomain(domain) {
|
|
|
1460
1658
|
if (!domain) return null;
|
|
1461
1659
|
const host = extractHost(domain);
|
|
1462
1660
|
if (!host) return null;
|
|
1661
|
+
if (isFreeEmailDomain(host)) return null;
|
|
1463
1662
|
return firstMatch(
|
|
1464
1663
|
"companies",
|
|
1465
1664
|
`filter=domainName.primaryLinkUrl[like]:%25${encodeURIComponent(host)}%25`
|
|
1466
1665
|
);
|
|
1467
1666
|
}
|
|
1667
|
+
function isCompanyDomain(domain) {
|
|
1668
|
+
const host = extractHost(domain);
|
|
1669
|
+
return host.length > 0 && !isFreeEmailDomain(host);
|
|
1670
|
+
}
|
|
1468
1671
|
function extractHost(url) {
|
|
1469
1672
|
try {
|
|
1470
1673
|
return new URL(url).hostname.replace(/^www\./, "");
|
|
@@ -1585,6 +1788,17 @@ var UpsertRecordStage = class {
|
|
|
1585
1788
|
if (companyName && !standard["name"]) standard["name"] = companyName;
|
|
1586
1789
|
if (!standard["domainName"] && assembled["domainName"]) standard["domainName"] = assembled["domainName"];
|
|
1587
1790
|
if (!standard["address"] && assembled["address"]) standard["address"] = assembled["address"];
|
|
1791
|
+
const proposedDomain = typeof standard["domainName"] === "string" ? standard["domainName"] : standard["domainName"]?.["primaryLinkUrl"];
|
|
1792
|
+
if (typeof proposedDomain === "string" && proposedDomain && !isCompanyDomain(proposedDomain)) {
|
|
1793
|
+
delete standard["domainName"];
|
|
1794
|
+
ctx.warnings.push(
|
|
1795
|
+
`"${extractHost(proposedDomain)}" is an email provider, not a company domain \u2014 it was left off the company record.`
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
if (!standard["name"] && !standard["domainName"]) {
|
|
1799
|
+
ctx.warnings.push("No company name or domain in this payload \u2014 no company was created.");
|
|
1800
|
+
return void 0;
|
|
1801
|
+
}
|
|
1588
1802
|
const domain = standard["domainName"]?.["primaryLinkUrl"];
|
|
1589
1803
|
let companyId;
|
|
1590
1804
|
if (domain) {
|