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