twenty-app-intake 0.3.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 +384 -8
- package/manifest.json +424 -21
- package/package.json +1 -1
- package/src/front-components/settings-panel.mjs +7 -7
- package/src/front-components/settings-panel.mjs.map +2 -2
- package/src/front-components/settings-panel.tsx +49 -0
- package/src/logic-functions/check-silence.mjs +263 -0
- package/src/logic-functions/check-silence.mjs.map +7 -0
- package/src/logic-functions/check-silence.ts +52 -0
- package/src/logic-functions/contract.mjs +507 -0
- package/src/logic-functions/contract.mjs.map +7 -0
- package/src/logic-functions/contract.ts +172 -0
- package/src/logic-functions/health.mjs +125 -5
- package/src/logic-functions/health.mjs.map +3 -3
- package/src/logic-functions/health.ts +41 -5
- package/src/logic-functions/quarantine-discard.mjs +380 -0
- package/src/logic-functions/quarantine-discard.mjs.map +7 -0
- package/src/logic-functions/quarantine-discard.ts +65 -0
- package/src/logic-functions/quarantine-list.mjs +211 -0
- package/src/logic-functions/quarantine-list.mjs.map +7 -0
- package/src/logic-functions/quarantine-list.ts +93 -0
- package/src/logic-functions/quarantine-release.mjs +2188 -0
- package/src/logic-functions/quarantine-release.mjs.map +7 -0
- package/src/logic-functions/quarantine-release.ts +94 -0
- package/src/logic-functions/register.mjs +34 -1
- package/src/logic-functions/register.mjs.map +2 -2
- package/src/logic-functions/replay-bulk.mjs +2254 -0
- package/src/logic-functions/replay-bulk.mjs.map +7 -0
- package/src/logic-functions/replay-bulk.ts +83 -0
- package/src/logic-functions/replay-log.mjs +2201 -0
- package/src/logic-functions/replay-log.mjs.map +7 -0
- package/src/logic-functions/replay-log.ts +54 -0
- package/src/logic-functions/retry.mjs +1236 -269
- package/src/logic-functions/retry.mjs.map +4 -4
- package/src/logic-functions/retry.ts +45 -68
- package/src/logic-functions/test-ingest.mjs +851 -48
- package/src/logic-functions/test-ingest.mjs.map +4 -4
- package/src/logic-functions/test-ingest.ts +136 -28
- package/src/logic-functions/webhook.mjs +1200 -230
- package/src/logic-functions/webhook.mjs.map +4 -4
- package/src/logic-functions/webhook.ts +23 -48
- package/src/post-install.mjs +34 -8
- package/src/post-install.mjs.map +2 -2
- package/src/post-install.ts +8 -8
|
@@ -21,6 +21,9 @@ var __anyHandler = {
|
|
|
21
21
|
var __anyStub = new Proxy(() => void 0, __anyHandler);
|
|
22
22
|
var defineLogicFunction = __defineFactoryStub;
|
|
23
23
|
|
|
24
|
+
// src/logic-functions/test-ingest.ts
|
|
25
|
+
import { CoreApiClient } from "twenty-client-sdk/core";
|
|
26
|
+
|
|
24
27
|
// src/lib/flattener.ts
|
|
25
28
|
var MAX_DEPTH = 8;
|
|
26
29
|
function flatten(input, prefix = "", depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
@@ -77,7 +80,7 @@ function detectStructure(raw) {
|
|
|
77
80
|
|
|
78
81
|
// src/constants/field-map.ts
|
|
79
82
|
function normaliseKey(key) {
|
|
80
|
-
return key.replace(/([A-Z])/g, "
|
|
83
|
+
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, "_");
|
|
81
84
|
}
|
|
82
85
|
var FIELD_MAP = {
|
|
83
86
|
// ── Phone ─────────────────────────────────────────────────────────────────
|
|
@@ -277,16 +280,163 @@ function classify(key, value) {
|
|
|
277
280
|
return "field";
|
|
278
281
|
}
|
|
279
282
|
|
|
283
|
+
// src/lib/merge.ts
|
|
284
|
+
function decideMerge(existing, incoming, policy, strategy = "INHERIT") {
|
|
285
|
+
if (isEmpty(incoming)) return { write: false, status: "preserved" };
|
|
286
|
+
if (isEmpty(existing)) return { write: true, status: "filled-empty" };
|
|
287
|
+
if (sameValue(existing, incoming)) return { write: false, status: "unchanged" };
|
|
288
|
+
const effective = strategy === "INHERIT" ? policy : strategy;
|
|
289
|
+
return effective === "NEWEST_WINS" ? { write: true, status: "set" } : { write: false, status: "preserved" };
|
|
290
|
+
}
|
|
291
|
+
function mergePatch(existing, incoming, policy, strategyFor = () => "INHERIT") {
|
|
292
|
+
const patch = {};
|
|
293
|
+
const decisions = {};
|
|
294
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
295
|
+
const decision = decideMerge(existing[key], value, policy, strategyFor(key));
|
|
296
|
+
decisions[key] = decision;
|
|
297
|
+
if (decision.write) patch[key] = value;
|
|
298
|
+
}
|
|
299
|
+
return { patch, decisions };
|
|
300
|
+
}
|
|
301
|
+
function parseFieldMergeStrategy(raw) {
|
|
302
|
+
switch ((raw ?? "").toUpperCase()) {
|
|
303
|
+
case "PRESERVE":
|
|
304
|
+
return "PRESERVE";
|
|
305
|
+
case "NEWEST_WINS":
|
|
306
|
+
return "NEWEST_WINS";
|
|
307
|
+
default:
|
|
308
|
+
return "INHERIT";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function isEmpty(value) {
|
|
312
|
+
if (value === null || value === void 0) return true;
|
|
313
|
+
if (typeof value === "string") return value.trim() === "";
|
|
314
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
315
|
+
if (typeof value === "object") {
|
|
316
|
+
const entries = Object.entries(value);
|
|
317
|
+
if (entries.length === 0) return true;
|
|
318
|
+
const meaningful = entries.filter(([k]) => !IGNORED_COMPOSITE_KEYS.has(k));
|
|
319
|
+
if (meaningful.length === 0) return true;
|
|
320
|
+
return meaningful.every(([, v]) => isEmpty(v));
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
var IGNORED_COMPOSITE_KEYS = /* @__PURE__ */ new Set([
|
|
325
|
+
"currencyCode",
|
|
326
|
+
"primaryLinkLabel",
|
|
327
|
+
"primaryPhoneCountryCode",
|
|
328
|
+
"primaryPhoneCallingCode",
|
|
329
|
+
"additionalLinks",
|
|
330
|
+
"additionalEmails",
|
|
331
|
+
"additionalPhones"
|
|
332
|
+
]);
|
|
333
|
+
function sameValue(a, b) {
|
|
334
|
+
if (a === b) return true;
|
|
335
|
+
if (typeof a === "object" && typeof b === "object" && a !== null && b !== null) {
|
|
336
|
+
try {
|
|
337
|
+
return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b));
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return String(a) === String(b);
|
|
343
|
+
}
|
|
344
|
+
function sortKeys(value) {
|
|
345
|
+
if (value === null || typeof value !== "object") return value;
|
|
346
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
347
|
+
const obj = value;
|
|
348
|
+
const out = {};
|
|
349
|
+
for (const key of Object.keys(obj).sort()) out[key] = sortKeys(obj[key]);
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
280
353
|
// src/lib/normalizer.ts
|
|
281
|
-
function
|
|
282
|
-
|
|
354
|
+
function actionForRule(rule) {
|
|
355
|
+
if (rule.action) return rule.action;
|
|
356
|
+
switch ((rule.fieldType ?? "").toUpperCase()) {
|
|
357
|
+
case "SKIP":
|
|
358
|
+
return "skip";
|
|
359
|
+
case "NOTE":
|
|
360
|
+
return "note";
|
|
361
|
+
default:
|
|
362
|
+
return "field";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function typeForRule(rule) {
|
|
366
|
+
switch ((rule.fieldType ?? "").toUpperCase()) {
|
|
367
|
+
case "NOTE":
|
|
368
|
+
return "RICH_TEXT";
|
|
369
|
+
case "SKIP":
|
|
370
|
+
return "TEXT";
|
|
371
|
+
case "":
|
|
372
|
+
return "TEXT";
|
|
373
|
+
default:
|
|
374
|
+
return rule.fieldType;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
var OPPORTUNITY_KEY_PREFIXES = ["opportunity_", "opp_", "deal_"];
|
|
378
|
+
function parseTargetObject(raw) {
|
|
379
|
+
switch ((raw ?? "").toUpperCase()) {
|
|
380
|
+
case "PERSON":
|
|
381
|
+
return "person";
|
|
382
|
+
case "COMPANY":
|
|
383
|
+
return "company";
|
|
384
|
+
case "OPPORTUNITY":
|
|
385
|
+
return "opportunity";
|
|
386
|
+
default:
|
|
387
|
+
return "auto";
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
var PROTECTED_CANONICAL_NAMES = /* @__PURE__ */ new Set([
|
|
391
|
+
"email",
|
|
392
|
+
"phone",
|
|
393
|
+
"firstName",
|
|
394
|
+
"lastName",
|
|
395
|
+
"fullName"
|
|
396
|
+
]);
|
|
397
|
+
function compileRule(rule) {
|
|
398
|
+
try {
|
|
399
|
+
return { ...rule, matcher: new RegExp(rule.inputPattern, "i") };
|
|
400
|
+
} catch {
|
|
401
|
+
return { ...rule, matcher: null };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function ruleMatches(rule, key, lookupKey) {
|
|
405
|
+
if (rule.inputPattern === key || rule.inputPattern === lookupKey) return true;
|
|
406
|
+
if (!rule.matcher) return false;
|
|
407
|
+
return rule.matcher.test(key) || rule.matcher.test(lookupKey);
|
|
408
|
+
}
|
|
409
|
+
function normalizeFields(flat, rules = [], warnings = []) {
|
|
410
|
+
const compiled = [...rules].sort((a, b) => b.priority - a.priority).map(compileRule);
|
|
283
411
|
return Object.entries(flat).map(
|
|
284
|
-
([key, value]) => normalizeField(key, value,
|
|
412
|
+
([key, value]) => normalizeField(key, value, compiled, warnings)
|
|
285
413
|
);
|
|
286
414
|
}
|
|
287
|
-
function normalizeField(key, value, rules) {
|
|
415
|
+
function normalizeField(key, value, rules, warnings) {
|
|
288
416
|
const lookupKey = normaliseKey(key);
|
|
289
417
|
const builtIn = FIELD_MAP[lookupKey];
|
|
418
|
+
for (const rule of rules) {
|
|
419
|
+
if (!ruleMatches(rule, key, lookupKey)) continue;
|
|
420
|
+
if (builtIn && PROTECTED_CANONICAL_NAMES.has(builtIn.canonicalName)) {
|
|
421
|
+
const displaces = rule.canonicalName !== builtIn.canonicalName || actionForRule(rule) !== "field";
|
|
422
|
+
if (displaces) {
|
|
423
|
+
warnings.push(
|
|
424
|
+
`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.`
|
|
425
|
+
);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
originalKey: key,
|
|
431
|
+
canonicalName: rule.canonicalName,
|
|
432
|
+
twentyType: typeForRule(rule),
|
|
433
|
+
action: actionForRule(rule),
|
|
434
|
+
value,
|
|
435
|
+
source: "rule",
|
|
436
|
+
targetObject: parseTargetObject(rule.targetObject),
|
|
437
|
+
mergeStrategy: parseFieldMergeStrategy(rule.mergeStrategy)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
290
440
|
if (builtIn) {
|
|
291
441
|
return {
|
|
292
442
|
originalKey: key,
|
|
@@ -294,37 +444,24 @@ function normalizeField(key, value, rules) {
|
|
|
294
444
|
twentyType: builtIn.twentyType,
|
|
295
445
|
action: builtIn.action,
|
|
296
446
|
value,
|
|
297
|
-
source: "builtin"
|
|
447
|
+
source: "builtin",
|
|
448
|
+
targetObject: "auto"
|
|
298
449
|
};
|
|
299
450
|
}
|
|
300
|
-
for (const rule of rules) {
|
|
301
|
-
let matches = false;
|
|
302
|
-
try {
|
|
303
|
-
matches = rule.inputPattern === key || rule.inputPattern === lookupKey || new RegExp(rule.inputPattern, "i").test(key);
|
|
304
|
-
} catch {
|
|
305
|
-
matches = rule.inputPattern === key;
|
|
306
|
-
}
|
|
307
|
-
if (matches) {
|
|
308
|
-
return {
|
|
309
|
-
originalKey: key,
|
|
310
|
-
canonicalName: rule.canonicalName,
|
|
311
|
-
twentyType: rule.fieldType,
|
|
312
|
-
action: rule.action,
|
|
313
|
-
value,
|
|
314
|
-
source: "rule"
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
451
|
const detectedType = detectType(value);
|
|
319
452
|
const classification = classify(key, value);
|
|
320
|
-
const
|
|
453
|
+
const prefix = OPPORTUNITY_KEY_PREFIXES.find((p) => lookupKey.startsWith(p));
|
|
454
|
+
const targetObject = prefix ? "opportunity" : "auto";
|
|
455
|
+
const bareKey = prefix ? lookupKey.slice(prefix.length) : key;
|
|
456
|
+
const canonicalName = classification === "field" ? toExtFieldName(bareKey) : lookupKey;
|
|
321
457
|
return {
|
|
322
458
|
originalKey: key,
|
|
323
459
|
canonicalName,
|
|
324
460
|
twentyType: detectedType,
|
|
325
461
|
action: classification === "field" ? "field" : classification,
|
|
326
462
|
value,
|
|
327
|
-
source: "passthrough"
|
|
463
|
+
source: "passthrough",
|
|
464
|
+
targetObject
|
|
328
465
|
};
|
|
329
466
|
}
|
|
330
467
|
function toExtFieldName(key) {
|
|
@@ -488,54 +625,720 @@ var coreApi = {
|
|
|
488
625
|
post: (path, body) => request(`${apiUrl()}/rest`, "POST", path, body),
|
|
489
626
|
patch: (path, body) => request(`${apiUrl()}/rest`, "PATCH", path, body)
|
|
490
627
|
};
|
|
628
|
+
var metaApi = {
|
|
629
|
+
get: (path) => request(`${apiUrl()}/rest/metadata`, "GET", path),
|
|
630
|
+
post: (path, body) => request(`${apiUrl()}/rest/metadata`, "POST", path, body)
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// src/constants/free-email-domains.ts
|
|
634
|
+
var FREE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
635
|
+
// Google
|
|
636
|
+
"gmail.com",
|
|
637
|
+
"googlemail.com",
|
|
638
|
+
// Microsoft
|
|
639
|
+
"outlook.com",
|
|
640
|
+
"outlook.co.uk",
|
|
641
|
+
"outlook.fr",
|
|
642
|
+
"outlook.de",
|
|
643
|
+
"outlook.es",
|
|
644
|
+
"hotmail.com",
|
|
645
|
+
"hotmail.co.uk",
|
|
646
|
+
"hotmail.fr",
|
|
647
|
+
"hotmail.de",
|
|
648
|
+
"hotmail.it",
|
|
649
|
+
"hotmail.es",
|
|
650
|
+
"live.com",
|
|
651
|
+
"live.co.uk",
|
|
652
|
+
"live.nl",
|
|
653
|
+
"msn.com",
|
|
654
|
+
"passport.com",
|
|
655
|
+
// Yahoo
|
|
656
|
+
"yahoo.com",
|
|
657
|
+
"yahoo.co.uk",
|
|
658
|
+
"yahoo.co.jp",
|
|
659
|
+
"yahoo.ca",
|
|
660
|
+
"yahoo.com.au",
|
|
661
|
+
"yahoo.de",
|
|
662
|
+
"yahoo.fr",
|
|
663
|
+
"yahoo.es",
|
|
664
|
+
"yahoo.it",
|
|
665
|
+
"yahoo.in",
|
|
666
|
+
"ymail.com",
|
|
667
|
+
"rocketmail.com",
|
|
668
|
+
// Apple
|
|
669
|
+
"icloud.com",
|
|
670
|
+
"me.com",
|
|
671
|
+
"mac.com",
|
|
672
|
+
// AOL / Verizon / legacy US ISPs — common on older consumer accounts
|
|
673
|
+
"aol.com",
|
|
674
|
+
"aim.com",
|
|
675
|
+
"verizon.net",
|
|
676
|
+
"att.net",
|
|
677
|
+
"sbcglobal.net",
|
|
678
|
+
"bellsouth.net",
|
|
679
|
+
"comcast.net",
|
|
680
|
+
"cox.net",
|
|
681
|
+
"charter.net",
|
|
682
|
+
"earthlink.net",
|
|
683
|
+
"juno.com",
|
|
684
|
+
"netzero.net",
|
|
685
|
+
"roadrunner.com",
|
|
686
|
+
"rr.com",
|
|
687
|
+
"optonline.net",
|
|
688
|
+
"frontier.com",
|
|
689
|
+
"windstream.net",
|
|
690
|
+
// Privacy-oriented providers
|
|
691
|
+
"proton.me",
|
|
692
|
+
"protonmail.com",
|
|
693
|
+
"protonmail.ch",
|
|
694
|
+
"pm.me",
|
|
695
|
+
"tutanota.com",
|
|
696
|
+
"tutanota.de",
|
|
697
|
+
"tuta.io",
|
|
698
|
+
"tuta.com",
|
|
699
|
+
"hushmail.com",
|
|
700
|
+
"fastmail.com",
|
|
701
|
+
"fastmail.fm",
|
|
702
|
+
"posteo.de",
|
|
703
|
+
"mailbox.org",
|
|
704
|
+
// Other large free providers
|
|
705
|
+
"gmx.com",
|
|
706
|
+
"gmx.net",
|
|
707
|
+
"gmx.de",
|
|
708
|
+
"gmx.at",
|
|
709
|
+
"gmx.ch",
|
|
710
|
+
"mail.com",
|
|
711
|
+
"email.com",
|
|
712
|
+
"usa.com",
|
|
713
|
+
"inbox.com",
|
|
714
|
+
"zoho.com",
|
|
715
|
+
"zohomail.com",
|
|
716
|
+
"yandex.com",
|
|
717
|
+
"yandex.ru",
|
|
718
|
+
"ya.ru",
|
|
719
|
+
"mail.ru",
|
|
720
|
+
"bk.ru",
|
|
721
|
+
"list.ru",
|
|
722
|
+
"inbox.ru",
|
|
723
|
+
"rambler.ru",
|
|
724
|
+
"qq.com",
|
|
725
|
+
"foxmail.com",
|
|
726
|
+
"163.com",
|
|
727
|
+
"126.com",
|
|
728
|
+
"sina.com",
|
|
729
|
+
"sina.cn",
|
|
730
|
+
"sohu.com",
|
|
731
|
+
"naver.com",
|
|
732
|
+
"hanmail.net",
|
|
733
|
+
"daum.net",
|
|
734
|
+
"nate.com",
|
|
735
|
+
"seznam.cz",
|
|
736
|
+
"wp.pl",
|
|
737
|
+
"o2.pl",
|
|
738
|
+
"onet.pl",
|
|
739
|
+
"interia.pl",
|
|
740
|
+
"libero.it",
|
|
741
|
+
"virgilio.it",
|
|
742
|
+
"alice.it",
|
|
743
|
+
"tiscali.it",
|
|
744
|
+
"orange.fr",
|
|
745
|
+
"wanadoo.fr",
|
|
746
|
+
"free.fr",
|
|
747
|
+
"laposte.net",
|
|
748
|
+
"sfr.fr",
|
|
749
|
+
"bbox.fr",
|
|
750
|
+
"neuf.fr",
|
|
751
|
+
"web.de",
|
|
752
|
+
"t-online.de",
|
|
753
|
+
"freenet.de",
|
|
754
|
+
"arcor.de",
|
|
755
|
+
"btinternet.com",
|
|
756
|
+
"sky.com",
|
|
757
|
+
"talktalk.net",
|
|
758
|
+
"virginmedia.com",
|
|
759
|
+
"ntlworld.com",
|
|
760
|
+
"bigpond.com",
|
|
761
|
+
"bigpond.net.au",
|
|
762
|
+
"optusnet.com.au",
|
|
763
|
+
"iinet.net.au",
|
|
764
|
+
"xtra.co.nz",
|
|
765
|
+
"terra.com.br",
|
|
766
|
+
"uol.com.br",
|
|
767
|
+
"bol.com.br",
|
|
768
|
+
"ig.com.br",
|
|
769
|
+
"rediffmail.com",
|
|
770
|
+
"sify.com",
|
|
771
|
+
"indiatimes.com",
|
|
772
|
+
// Throwaway / disposable services — never a real employer
|
|
773
|
+
"mailinator.com",
|
|
774
|
+
"guerrillamail.com",
|
|
775
|
+
"guerrillamail.net",
|
|
776
|
+
"sharklasers.com",
|
|
777
|
+
"10minutemail.com",
|
|
778
|
+
"temp-mail.org",
|
|
779
|
+
"tempmail.com",
|
|
780
|
+
"throwawaymail.com",
|
|
781
|
+
"yopmail.com",
|
|
782
|
+
"trashmail.com",
|
|
783
|
+
"getnada.com",
|
|
784
|
+
"dispostable.com",
|
|
785
|
+
"maildrop.cc",
|
|
786
|
+
"mintemail.com",
|
|
787
|
+
"fakeinbox.com",
|
|
788
|
+
"spamgourmet.com",
|
|
789
|
+
"mailnesia.com",
|
|
790
|
+
"moakt.com",
|
|
791
|
+
"emailondeck.com",
|
|
792
|
+
"burnermail.io",
|
|
793
|
+
"anonaddy.com",
|
|
794
|
+
"simplelogin.io",
|
|
795
|
+
"duck.com",
|
|
796
|
+
"relay.firefox.com",
|
|
797
|
+
// Placeholder domains that show up in test and pasted payloads
|
|
798
|
+
"example.com",
|
|
799
|
+
"example.org",
|
|
800
|
+
"example.net",
|
|
801
|
+
"test.com",
|
|
802
|
+
"localhost"
|
|
803
|
+
]);
|
|
804
|
+
function isFreeEmailDomain(host) {
|
|
805
|
+
const normalised = host.trim().toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
|
|
806
|
+
if (!normalised) return false;
|
|
807
|
+
return FREE_EMAIL_DOMAINS.has(normalised);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/lib/lookup.ts
|
|
811
|
+
async function firstMatch(collection, query) {
|
|
812
|
+
try {
|
|
813
|
+
const res = await coreApi.get(`/${collection}?${query}&first=1`);
|
|
814
|
+
return res.data?.[collection]?.edges?.[0]?.node ?? null;
|
|
815
|
+
} catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async function findPersonByEmail(email) {
|
|
820
|
+
if (!email) return null;
|
|
821
|
+
return firstMatch("people", `filter=emails.primaryEmail[eq]:${encodeURIComponent(email)}`);
|
|
822
|
+
}
|
|
823
|
+
async function findCompanyByDomain(domain) {
|
|
824
|
+
if (!domain) return null;
|
|
825
|
+
const host = extractHost(domain);
|
|
826
|
+
if (!host) return null;
|
|
827
|
+
if (isFreeEmailDomain(host)) return null;
|
|
828
|
+
return firstMatch(
|
|
829
|
+
"companies",
|
|
830
|
+
`filter=domainName.primaryLinkUrl[like]:%25${encodeURIComponent(host)}%25`
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
function isCompanyDomain(domain) {
|
|
834
|
+
const host = extractHost(domain);
|
|
835
|
+
return host.length > 0 && !isFreeEmailDomain(host);
|
|
836
|
+
}
|
|
837
|
+
function extractHost(url) {
|
|
838
|
+
try {
|
|
839
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
840
|
+
} catch {
|
|
841
|
+
return url;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function primaryEmailOf(assembled) {
|
|
845
|
+
const emails = assembled["emails"];
|
|
846
|
+
const value = emails?.["primaryEmail"];
|
|
847
|
+
return typeof value === "string" ? value : "";
|
|
848
|
+
}
|
|
849
|
+
function primaryDomainOf(standard) {
|
|
850
|
+
const domainName = standard["domainName"];
|
|
851
|
+
if (typeof domainName === "string") return domainName;
|
|
852
|
+
const value = domainName?.["primaryLinkUrl"];
|
|
853
|
+
return typeof value === "string" ? value : "";
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/lib/diff.ts
|
|
857
|
+
var STANDARD_BY_OBJECT = {
|
|
858
|
+
person: /* @__PURE__ */ new Set([
|
|
859
|
+
"name",
|
|
860
|
+
"emails",
|
|
861
|
+
"phones",
|
|
862
|
+
"firstName",
|
|
863
|
+
"lastName",
|
|
864
|
+
"fullName",
|
|
865
|
+
"email",
|
|
866
|
+
"phone",
|
|
867
|
+
"linkedInLink",
|
|
868
|
+
"xLink",
|
|
869
|
+
"jobTitle",
|
|
870
|
+
"city",
|
|
871
|
+
"companyName",
|
|
872
|
+
"domainName",
|
|
873
|
+
"address"
|
|
874
|
+
]),
|
|
875
|
+
company: /* @__PURE__ */ new Set([
|
|
876
|
+
"name",
|
|
877
|
+
"domainName",
|
|
878
|
+
"address",
|
|
879
|
+
"companyName",
|
|
880
|
+
"linkedInLink",
|
|
881
|
+
"xLink",
|
|
882
|
+
"employees",
|
|
883
|
+
"annualRecurringRevenue",
|
|
884
|
+
"idealCustomerProfile",
|
|
885
|
+
"city"
|
|
886
|
+
]),
|
|
887
|
+
opportunity: /* @__PURE__ */ new Set(["name", "stage", "amount", "closeDate"])
|
|
888
|
+
};
|
|
889
|
+
async function buildDiff(input) {
|
|
890
|
+
const clean = input.crmFields.filter((f) => !f.originalKey.startsWith("__"));
|
|
891
|
+
const { assembled, remainder } = assembleComposites(clean);
|
|
892
|
+
for (const f of remainder) assembled[f.canonicalName] = f.value;
|
|
893
|
+
const objects = [];
|
|
894
|
+
const existingFields = await objectFieldNames();
|
|
895
|
+
const strategyFor = (field) => clean.find((f) => f.canonicalName === field)?.mergeStrategy ?? "INHERIT";
|
|
896
|
+
const companyName = input.structuredCompany?.["name"] ?? assembled["companyName"];
|
|
897
|
+
if (companyName || input.targetObject === "COMPANY") {
|
|
898
|
+
const standard = { ...input.structuredCompany ?? {} };
|
|
899
|
+
if (companyName && !standard["name"]) standard["name"] = companyName;
|
|
900
|
+
if (!standard["domainName"] && assembled["domainName"]) standard["domainName"] = assembled["domainName"];
|
|
901
|
+
if (!standard["address"] && assembled["address"]) standard["address"] = assembled["address"];
|
|
902
|
+
const proposedDomain = primaryDomainOf(standard);
|
|
903
|
+
if (proposedDomain && !isCompanyDomain(proposedDomain)) {
|
|
904
|
+
delete standard["domainName"];
|
|
905
|
+
input.warnings.push(
|
|
906
|
+
`"${extractHost(proposedDomain)}" is an email provider, not a company domain \u2014 it was left off the company record.`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
if (!standard["name"] && !standard["domainName"]) {
|
|
910
|
+
input.warnings.push("No company name or domain in this payload \u2014 no company was created.");
|
|
911
|
+
} else {
|
|
912
|
+
const domain = primaryDomainOf(standard);
|
|
913
|
+
const existing = domain ? await findCompanyByDomain(domain) : null;
|
|
914
|
+
objects.push(describe(
|
|
915
|
+
"company",
|
|
916
|
+
existing,
|
|
917
|
+
domain ? { field: "domainName", value: domain } : null,
|
|
918
|
+
{ ...standard, ...customFor(clean, "company") },
|
|
919
|
+
clean,
|
|
920
|
+
existingFields["company"] ?? /* @__PURE__ */ new Set(),
|
|
921
|
+
input.policy,
|
|
922
|
+
strategyFor
|
|
923
|
+
));
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const hasPersonData = assembled["emails"] || assembled["name"] || input.structuredPerson;
|
|
927
|
+
if (hasPersonData || input.targetObject === "PERSON") {
|
|
928
|
+
const standard = { ...input.structuredPerson ?? {} };
|
|
929
|
+
if (!standard["name"] && assembled["name"]) standard["name"] = assembled["name"];
|
|
930
|
+
if (!standard["emails"] && assembled["emails"]) standard["emails"] = assembled["emails"];
|
|
931
|
+
if (!standard["phones"] && assembled["phones"]) standard["phones"] = assembled["phones"];
|
|
932
|
+
const email = primaryEmailOf(standard) || primaryEmailOf(assembled);
|
|
933
|
+
const existing = email ? await findPersonByEmail(email) : null;
|
|
934
|
+
objects.push(describe(
|
|
935
|
+
"person",
|
|
936
|
+
existing,
|
|
937
|
+
email ? { field: "emails.primaryEmail", value: email } : null,
|
|
938
|
+
{ ...standard, ...customFor(clean, "person") },
|
|
939
|
+
clean,
|
|
940
|
+
existingFields["person"] ?? /* @__PURE__ */ new Set(),
|
|
941
|
+
input.policy,
|
|
942
|
+
strategyFor
|
|
943
|
+
));
|
|
944
|
+
}
|
|
945
|
+
if (input.createOpportunity && objects.some((o) => o.operation !== "skip")) {
|
|
946
|
+
const oppCustom = customFor(clean, "opportunity");
|
|
947
|
+
const oppStandard = {};
|
|
948
|
+
for (const f of clean) {
|
|
949
|
+
if (f.targetObject === "opportunity" && (f.canonicalName === "amount" || f.canonicalName === "closeDate")) {
|
|
950
|
+
oppStandard[f.canonicalName] = f.value;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
objects.push(describe(
|
|
954
|
+
"opportunity",
|
|
955
|
+
null,
|
|
956
|
+
null,
|
|
957
|
+
{ ...oppStandard, ...oppCustom },
|
|
958
|
+
clean,
|
|
959
|
+
existingFields["opportunity"] ?? /* @__PURE__ */ new Set(),
|
|
960
|
+
input.policy,
|
|
961
|
+
strategyFor
|
|
962
|
+
));
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
objects,
|
|
966
|
+
noteFields: input.noteFields.map((f) => f.canonicalName),
|
|
967
|
+
skipped: input.skipped.map((f) => f.originalKey),
|
|
968
|
+
warnings: input.warnings
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function customFor(fields, object) {
|
|
972
|
+
const out = {};
|
|
973
|
+
for (const f of fields) {
|
|
974
|
+
const target = f.targetObject === "auto" ? impliedPrimary(fields) : f.targetObject;
|
|
975
|
+
if (target !== object) continue;
|
|
976
|
+
if (STANDARD_BY_OBJECT[object]?.has(f.canonicalName)) continue;
|
|
977
|
+
out[f.canonicalName] = f.value;
|
|
978
|
+
}
|
|
979
|
+
return out;
|
|
980
|
+
}
|
|
981
|
+
function impliedPrimary(fields) {
|
|
982
|
+
const hasPerson = fields.some((f) => ["firstName", "lastName", "email", "phone", "fullName", "emails", "phones", "name"].includes(f.canonicalName));
|
|
983
|
+
return hasPerson ? "person" : "company";
|
|
984
|
+
}
|
|
985
|
+
function describe(object, existing, matchedBy, incoming, normalized, objectFields, policy, strategyFor) {
|
|
986
|
+
const { decisions } = existing ? mergePatch(existing, incoming, policy, strategyFor) : { decisions: {} };
|
|
987
|
+
const fields = [];
|
|
988
|
+
const fieldsToCreate = [];
|
|
989
|
+
for (const [name, value] of Object.entries(incoming)) {
|
|
990
|
+
const source = normalized.find((f) => f.canonicalName === name);
|
|
991
|
+
const isStandard = STANDARD_BY_OBJECT[object]?.has(name) ?? false;
|
|
992
|
+
const wouldCreate = !isStandard && !objectFields.has(name);
|
|
993
|
+
if (wouldCreate) fieldsToCreate.push({ name, type: source?.twentyType ?? "TEXT" });
|
|
994
|
+
let status = "create";
|
|
995
|
+
if (existing) {
|
|
996
|
+
const decided = decisions[name]?.status;
|
|
997
|
+
status = decided === "filled-empty" ? "fill-empty" : decided === "unchanged" ? "unchanged" : decided === "set" ? "overwrite" : "preserved";
|
|
998
|
+
}
|
|
999
|
+
fields.push({
|
|
1000
|
+
name,
|
|
1001
|
+
from: source?.source ?? "derived",
|
|
1002
|
+
incoming: value,
|
|
1003
|
+
...existing ? { existing: existing[name] } : {},
|
|
1004
|
+
status,
|
|
1005
|
+
fieldWouldBeCreated: wouldCreate,
|
|
1006
|
+
twentyType: source?.twentyType
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
object,
|
|
1011
|
+
operation: existing ? "update" : "create",
|
|
1012
|
+
recordId: existing?.["id"] ?? null,
|
|
1013
|
+
matchedBy,
|
|
1014
|
+
fields,
|
|
1015
|
+
fieldsToCreate
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
async function objectFieldNames() {
|
|
1019
|
+
try {
|
|
1020
|
+
const res = await metaApi.get("/objects");
|
|
1021
|
+
const list = Array.isArray(res) ? res : res.data ?? [];
|
|
1022
|
+
const out = {};
|
|
1023
|
+
for (const node of list) {
|
|
1024
|
+
out[node.nameSingular] = new Set((node.fields ?? []).map((f) => f.name));
|
|
1025
|
+
}
|
|
1026
|
+
return out;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return {};
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/lib/spam.ts
|
|
1033
|
+
var HONEYPOT_SCORE = 100;
|
|
1034
|
+
var DISPOSABLE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
1035
|
+
"mailinator.com",
|
|
1036
|
+
"guerrillamail.com",
|
|
1037
|
+
"guerrillamail.net",
|
|
1038
|
+
"sharklasers.com",
|
|
1039
|
+
"10minutemail.com",
|
|
1040
|
+
"10minutemail.net",
|
|
1041
|
+
"tempmail.com",
|
|
1042
|
+
"temp-mail.org",
|
|
1043
|
+
"throwawaymail.com",
|
|
1044
|
+
"yopmail.com",
|
|
1045
|
+
"yopmail.net",
|
|
1046
|
+
"trashmail.com",
|
|
1047
|
+
"dispostable.com",
|
|
1048
|
+
"getnada.com",
|
|
1049
|
+
"nada.email",
|
|
1050
|
+
"fakeinbox.com",
|
|
1051
|
+
"maildrop.cc",
|
|
1052
|
+
"mintemail.com",
|
|
1053
|
+
"spamgourmet.com",
|
|
1054
|
+
"mailnesia.com",
|
|
1055
|
+
"tempinbox.com",
|
|
1056
|
+
"emailondeck.com",
|
|
1057
|
+
"moakt.com",
|
|
1058
|
+
"mohmal.com",
|
|
1059
|
+
"inboxkitten.com",
|
|
1060
|
+
"mailcatch.com",
|
|
1061
|
+
"mytemp.email",
|
|
1062
|
+
"discard.email"
|
|
1063
|
+
]);
|
|
1064
|
+
var PLACEHOLDER_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
1065
|
+
"example.com",
|
|
1066
|
+
"example.org",
|
|
1067
|
+
"example.net",
|
|
1068
|
+
"test.com",
|
|
1069
|
+
"localhost",
|
|
1070
|
+
"domain.com",
|
|
1071
|
+
"email.com",
|
|
1072
|
+
"yourdomain.com"
|
|
1073
|
+
]);
|
|
1074
|
+
var URL_RE2 = /https?:\/\/|www\./i;
|
|
1075
|
+
var GLOBAL_URL_RE = /https?:\/\/[^\s<>"']+/gi;
|
|
1076
|
+
var MARKUP_RE = /<\s*a\s|<\s*script|\[url[=\]]|\[link[=\]]|\]\(https?:/i;
|
|
1077
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
|
1078
|
+
var NAME_KEYS = /* @__PURE__ */ new Set([
|
|
1079
|
+
"name",
|
|
1080
|
+
"fullName",
|
|
1081
|
+
"firstName",
|
|
1082
|
+
"lastName",
|
|
1083
|
+
"contact_name",
|
|
1084
|
+
"your_name"
|
|
1085
|
+
]);
|
|
1086
|
+
var MESSAGE_KEYS = /* @__PURE__ */ new Set([
|
|
1087
|
+
"message",
|
|
1088
|
+
"description",
|
|
1089
|
+
"comments",
|
|
1090
|
+
"notes",
|
|
1091
|
+
"body",
|
|
1092
|
+
"details",
|
|
1093
|
+
"content",
|
|
1094
|
+
"text",
|
|
1095
|
+
"inquiry",
|
|
1096
|
+
"summary",
|
|
1097
|
+
"feedback"
|
|
1098
|
+
]);
|
|
1099
|
+
function scoreSpam(flat, honeypotKey) {
|
|
1100
|
+
const signals = [];
|
|
1101
|
+
if (honeypotKey) {
|
|
1102
|
+
const hit = Object.entries(flat).find(
|
|
1103
|
+
([k, v]) => k.toLowerCase() === honeypotKey.toLowerCase() && isFilled(v)
|
|
1104
|
+
);
|
|
1105
|
+
if (hit) {
|
|
1106
|
+
return {
|
|
1107
|
+
score: HONEYPOT_SCORE,
|
|
1108
|
+
signals: [{ reason: `Honeypot field "${hit[0]}" was filled`, points: HONEYPOT_SCORE }],
|
|
1109
|
+
honeypot: true
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const add = (reason, points) => signals.push({ reason, points });
|
|
1114
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
1115
|
+
if (typeof value !== "string" || value === "") continue;
|
|
1116
|
+
const bare = key.replace(/^(opportunity_|opp_|deal_)/i, "");
|
|
1117
|
+
if (NAME_KEYS.has(bare) || NAME_KEYS.has(key)) {
|
|
1118
|
+
if (URL_RE2.test(value)) add(`Name field "${key}" contains a URL`, 4);
|
|
1119
|
+
if (MARKUP_RE.test(value)) add(`Name field "${key}" contains markup or a link tag`, 4);
|
|
1120
|
+
}
|
|
1121
|
+
if (MESSAGE_KEYS.has(bare) || MESSAGE_KEYS.has(key)) {
|
|
1122
|
+
const urls = value.match(GLOBAL_URL_RE) ?? [];
|
|
1123
|
+
const distinct = new Set(urls.map((u) => u.toLowerCase()));
|
|
1124
|
+
if (distinct.size >= 5) add(`Message field "${key}" contains ${distinct.size} links`, 3);
|
|
1125
|
+
if (MARKUP_RE.test(value)) add(`Message field "${key}" contains anchor or BBCode markup`, 2);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const email = firstEmail(flat);
|
|
1129
|
+
if (email !== null) {
|
|
1130
|
+
if (!EMAIL_RE2.test(email)) {
|
|
1131
|
+
add(`Email "${truncate(email)}" is not a valid address`, 3);
|
|
1132
|
+
} else {
|
|
1133
|
+
const domain = email.slice(email.lastIndexOf("@") + 1).toLowerCase();
|
|
1134
|
+
if (DISPOSABLE_EMAIL_DOMAINS.has(domain)) {
|
|
1135
|
+
add(`Email domain "${domain}" is a disposable inbox`, 4);
|
|
1136
|
+
} else if (PLACEHOLDER_EMAIL_DOMAINS.has(domain)) {
|
|
1137
|
+
add(`Email domain "${domain}" is a placeholder domain`, 3);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const repeated = mostRepeatedValue(flat);
|
|
1142
|
+
if (repeated && repeated.count >= 4) {
|
|
1143
|
+
add(`The same value appears in ${repeated.count} different fields`, 2);
|
|
1144
|
+
}
|
|
1145
|
+
const phone = firstPhone(flat);
|
|
1146
|
+
if (phone && isImplausiblePhone(phone)) {
|
|
1147
|
+
add(`Phone "${truncate(phone)}" is a placeholder number`, 2);
|
|
1148
|
+
}
|
|
1149
|
+
const score = signals.reduce((sum, s) => sum + s.points, 0);
|
|
1150
|
+
return { score, signals, honeypot: false };
|
|
1151
|
+
}
|
|
1152
|
+
function describeVerdict(verdict) {
|
|
1153
|
+
return verdict.signals.map((s) => s.reason).join("; ").slice(0, 500);
|
|
1154
|
+
}
|
|
1155
|
+
function isFilled(value) {
|
|
1156
|
+
if (value === null || value === void 0) return false;
|
|
1157
|
+
if (typeof value === "string") return value.trim() !== "";
|
|
1158
|
+
if (typeof value === "boolean") return value;
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
function firstEmail(flat) {
|
|
1162
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
1163
|
+
if (typeof value !== "string" || value === "") continue;
|
|
1164
|
+
if (/mail/i.test(key)) return value.trim();
|
|
1165
|
+
}
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
function firstPhone(flat) {
|
|
1169
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
1170
|
+
if (typeof value !== "string" || value === "") continue;
|
|
1171
|
+
if (/phone|mobile|tel|cell/i.test(key)) return value.trim();
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
function isImplausiblePhone(raw) {
|
|
1176
|
+
const digits = raw.replace(/\D/g, "");
|
|
1177
|
+
if (digits.length < 7) return false;
|
|
1178
|
+
if (/^(\d)\1+$/.test(digits)) return true;
|
|
1179
|
+
const ascending = digits.split("").every((d, i, arr) => i === 0 || Number(d) === (Number(arr[i - 1]) + 1) % 10);
|
|
1180
|
+
return ascending;
|
|
1181
|
+
}
|
|
1182
|
+
function mostRepeatedValue(flat) {
|
|
1183
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1184
|
+
for (const value of Object.values(flat)) {
|
|
1185
|
+
if (typeof value !== "string") continue;
|
|
1186
|
+
const v = value.trim();
|
|
1187
|
+
if (v.length < 8) continue;
|
|
1188
|
+
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
1189
|
+
}
|
|
1190
|
+
let best = null;
|
|
1191
|
+
for (const [value, count] of counts) {
|
|
1192
|
+
if (!best || count > best.count) best = { value, count };
|
|
1193
|
+
}
|
|
1194
|
+
return best;
|
|
1195
|
+
}
|
|
1196
|
+
function truncate(s) {
|
|
1197
|
+
return s.length > 60 ? `${s.slice(0, 60)}\u2026` : s;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/lib/settings.ts
|
|
1201
|
+
function str(key, fallback) {
|
|
1202
|
+
const v = process.env[key];
|
|
1203
|
+
return v === void 0 || v === "" ? fallback : v;
|
|
1204
|
+
}
|
|
1205
|
+
function int(key, fallback) {
|
|
1206
|
+
const parsed = parseInt(process.env[key] ?? "", 10);
|
|
1207
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
1208
|
+
}
|
|
1209
|
+
function bool(key, fallback) {
|
|
1210
|
+
const v = process.env[key];
|
|
1211
|
+
if (v === void 0 || v === "") return fallback;
|
|
1212
|
+
return v.toLowerCase() === "true";
|
|
1213
|
+
}
|
|
1214
|
+
function mergePolicy() {
|
|
1215
|
+
return str("INTAKE_MERGE_POLICY", "PRESERVE").toUpperCase() === "NEWEST_WINS" ? "NEWEST_WINS" : "PRESERVE";
|
|
1216
|
+
}
|
|
1217
|
+
function spamFilterEnabled() {
|
|
1218
|
+
return bool("INTAKE_SPAM_FILTER_ENABLED", false);
|
|
1219
|
+
}
|
|
1220
|
+
function spamScoreThreshold() {
|
|
1221
|
+
return int("INTAKE_SPAM_SCORE_THRESHOLD", 5);
|
|
1222
|
+
}
|
|
1223
|
+
function honeypotField() {
|
|
1224
|
+
return str("INTAKE_HONEYPOT_FIELD", "");
|
|
1225
|
+
}
|
|
491
1226
|
|
|
492
1227
|
// src/logic-functions/test-ingest.ts
|
|
493
1228
|
var handler = async (params) => {
|
|
494
1229
|
const sourceSlug = params.pathParameters?.["slug"];
|
|
495
|
-
if (!sourceSlug) {
|
|
496
|
-
return { statusCode: 400, body: { error: "Missing source slug" } };
|
|
497
|
-
}
|
|
1230
|
+
if (!sourceSlug) return { statusCode: 400, body: { error: "Missing source slug" } };
|
|
498
1231
|
let rawPayload = {};
|
|
499
1232
|
if (params.body && typeof params.body === "object" && !Array.isArray(params.body)) {
|
|
500
1233
|
rawPayload = params.body;
|
|
501
1234
|
}
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
).catch(() => null);
|
|
505
|
-
const source = srcResult?.data?.intakeSources?.edges?.[0]?.node;
|
|
1235
|
+
const source = await loadSource(sourceSlug);
|
|
1236
|
+
const rules = await loadRules(source?.id);
|
|
506
1237
|
const structure = detectStructure(rawPayload);
|
|
507
|
-
const flat = structure.type === "flat" ? structure.data : flatten(structure.
|
|
508
|
-
const
|
|
1238
|
+
const flat = structure.type === "flat" ? structure.data : flatten(structure.extra ?? {});
|
|
1239
|
+
const warnings = [];
|
|
1240
|
+
const normalized = normalizeFields(flat, rules, warnings);
|
|
509
1241
|
const { crmFields, noteFields, skipped } = partition(normalized);
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
1242
|
+
const honeypot = source?.honeypotField || honeypotField();
|
|
1243
|
+
const verdict = scoreSpam(flat, honeypot);
|
|
1244
|
+
const wouldQuarantine = verdict.honeypot || spamFilterEnabled() && verdict.score >= spamScoreThreshold();
|
|
1245
|
+
if (!source) warnings.push(`No source registered with slug "${sourceSlug}" \u2014 previewing with default settings. A live request would return 404.`);
|
|
1246
|
+
if (source?.status === "PAUSED") warnings.push("This source is PAUSED \u2014 a live request would return 423.");
|
|
514
1247
|
const noteOverflow = {};
|
|
515
1248
|
for (const f of noteFields) noteOverflow[f.canonicalName] = f.value;
|
|
1249
|
+
const diff = wouldQuarantine ? null : await buildDiff({
|
|
1250
|
+
crmFields,
|
|
1251
|
+
noteFields,
|
|
1252
|
+
skipped,
|
|
1253
|
+
structuredCompany: structure.type === "structured" ? structure.company ?? void 0 : void 0,
|
|
1254
|
+
structuredPerson: structure.type === "structured" ? structure.person ?? void 0 : void 0,
|
|
1255
|
+
createOpportunity: source?.createOpportunity ?? true,
|
|
1256
|
+
targetObject: source?.targetObject ?? "AUTO",
|
|
1257
|
+
policy: parsePolicy(source?.mergePolicy) ?? mergePolicy(),
|
|
1258
|
+
warnings
|
|
1259
|
+
});
|
|
516
1260
|
return {
|
|
517
1261
|
statusCode: 200,
|
|
518
1262
|
body: {
|
|
519
1263
|
dryRun: true,
|
|
520
|
-
source: source ? { id: source.id, name: source.name } : null,
|
|
1264
|
+
source: source ? { id: source.id, name: source.name, slug: source.slug, status: source.status } : null,
|
|
521
1265
|
payloadHash: computePayloadHash(rawPayload),
|
|
522
1266
|
payloadStructure: structure.type,
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
1267
|
+
spam: {
|
|
1268
|
+
score: verdict.score,
|
|
1269
|
+
threshold: spamScoreThreshold(),
|
|
1270
|
+
filterEnabled: spamFilterEnabled(),
|
|
1271
|
+
honeypotField: honeypot || null,
|
|
1272
|
+
wouldQuarantine,
|
|
1273
|
+
signals: verdict.signals,
|
|
1274
|
+
reason: verdict.signals.length > 0 ? describeVerdict(verdict) : null
|
|
528
1275
|
},
|
|
529
|
-
|
|
1276
|
+
mergePolicy: parsePolicy(source?.mergePolicy) ?? mergePolicy(),
|
|
1277
|
+
/**
|
|
1278
|
+
* Per object: whether it would be created or updated, which record it
|
|
1279
|
+
* matched, what each field would do to the value already there, and which
|
|
1280
|
+
* fields do not exist yet and would be added to the schema.
|
|
1281
|
+
*/
|
|
1282
|
+
diff,
|
|
1283
|
+
note: noteFields.length > 0 ? { fields: noteFields.map((f) => f.canonicalName), preview: formatNote(source?.name ?? sourceSlug, noteOverflow) } : null,
|
|
1284
|
+
rulesApplied: rules.length,
|
|
1285
|
+
warnings,
|
|
530
1286
|
rawFlat: flat
|
|
531
1287
|
}
|
|
532
1288
|
};
|
|
533
1289
|
};
|
|
1290
|
+
async function loadSource(slug) {
|
|
1291
|
+
try {
|
|
1292
|
+
const res = await coreApi.get(
|
|
1293
|
+
`/intakeSources?filter=slug[eq]:${encodeURIComponent(slug)}&first=1`
|
|
1294
|
+
);
|
|
1295
|
+
return res.data?.intakeSources?.edges?.[0]?.node ?? null;
|
|
1296
|
+
} catch {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
async function loadRules(sourceId) {
|
|
1301
|
+
try {
|
|
1302
|
+
const client = new CoreApiClient();
|
|
1303
|
+
const filter = sourceId ? { or: [{ intakeSource: { id: { eq: sourceId } } }, { intakeSource: { id: { is: "NULL" } } }] } : { intakeSource: { id: { is: "NULL" } } };
|
|
1304
|
+
const result = await client.query({
|
|
1305
|
+
intakeFieldRules: {
|
|
1306
|
+
__args: {
|
|
1307
|
+
filter: { and: [filter, { isActive: { eq: true } }] },
|
|
1308
|
+
orderBy: { priority: "DescNullsLast" }
|
|
1309
|
+
},
|
|
1310
|
+
edges: {
|
|
1311
|
+
node: {
|
|
1312
|
+
inputPattern: true,
|
|
1313
|
+
canonicalName: true,
|
|
1314
|
+
fieldType: true,
|
|
1315
|
+
priority: true,
|
|
1316
|
+
targetObject: true,
|
|
1317
|
+
mergeStrategy: true
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
return (result.intakeFieldRules?.edges ?? []).map((e) => e.node);
|
|
1323
|
+
} catch {
|
|
1324
|
+
return [];
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
function parsePolicy(raw) {
|
|
1328
|
+
switch ((raw ?? "").toUpperCase()) {
|
|
1329
|
+
case "PRESERVE":
|
|
1330
|
+
return "PRESERVE";
|
|
1331
|
+
case "NEWEST_WINS":
|
|
1332
|
+
return "NEWEST_WINS";
|
|
1333
|
+
default:
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
534
1337
|
var test_ingest_default = defineLogicFunction({
|
|
535
1338
|
universalIdentifier: "3f7a9c21-8d4b-4e6f-a1c2-5b8d0f2e7a4c",
|
|
536
1339
|
name: "intake-test",
|
|
537
|
-
description: "Dry-run:
|
|
538
|
-
timeoutSeconds:
|
|
1340
|
+
description: "Dry-run: returns a structured diff of what would be created or updated, writing nothing.",
|
|
1341
|
+
timeoutSeconds: 20,
|
|
539
1342
|
handler,
|
|
540
1343
|
httpRouteTriggerSettings: {
|
|
541
1344
|
path: "/intake/:slug/test",
|