strapi-plugin-hubspot 0.13.1 → 0.14.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/CHANGELOG.md +25 -0
- package/README.md +42 -25
- package/dist/_chunks/{Forms-lrzuWzoB.js → Forms-Cch-7zei.js} +38 -3
- package/dist/_chunks/{Forms-CHB-sKem.mjs → Forms-ws6d8rdC.mjs} +38 -3
- package/dist/_chunks/{HubspotFormInput-YI09keTC.mjs → HubspotFormInput-18pWTITT.mjs} +1 -1
- package/dist/_chunks/{HubspotFormInput-Bb6it0nP.js → HubspotFormInput-M2l_8w_e.js} +1 -1
- package/dist/_chunks/{HubspotObjectInput-D3A6xzb3.js → HubspotObjectInput-BDO99qXs.js} +3 -3
- package/dist/_chunks/{HubspotObjectInput-CzLKT9oj.mjs → HubspotObjectInput-DshusXZP.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-DicskLcN.mjs → HubspotPropertyInput-C-kHDgwx.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-Dss3eUlL.js → HubspotPropertyInput-XGnH2tHn.js} +3 -3
- package/dist/_chunks/{Settings-4CbX3SPK.mjs → Settings-Bm7WkzK1.mjs} +2 -2
- package/dist/_chunks/{Settings-Ch8tPZAV.js → Settings-CRnsu6ob.js} +2 -2
- package/dist/_chunks/{en-PYFibnDc.js → en-BXMU5CjT.js} +4 -0
- package/dist/_chunks/{en-D8DJiSxd.mjs → en-DKV-8FWP.mjs} +4 -0
- package/dist/_chunks/{fr-yHk4sSEl.js → fr-Dx4XmZ4X.js} +4 -0
- package/dist/_chunks/{fr-BMjixfXn.mjs → fr-GB6T3ZyU.mjs} +4 -0
- package/dist/_chunks/{index-BAvedd2m.js → index-BlaG8oIq.js} +6 -6
- package/dist/_chunks/{index-BE2ribUL.mjs → index-CPVfElYB.mjs} +6 -6
- package/dist/_chunks/{objectLabels-DGJxYjKz.mjs → objectLabels-6OUnIDB5.mjs} +1 -1
- package/dist/_chunks/{objectLabels-CycoU0zY.js → objectLabels-CPc21Tdd.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-DRfaI6mF.js → useHubspotSchema-0MvCM1AC.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-e31UsYb2.mjs → useHubspotSchema-qCUs2gIX.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/builder/types.d.ts +2 -0
- package/dist/server/index.js +417 -189
- package/dist/server/index.mjs +417 -189
- package/dist/server/src/__tests__/hsforms.test.d.ts +1 -0
- package/dist/server/src/content-types.d.ts +8 -0
- package/dist/server/src/forms.d.ts +10 -6
- package/dist/server/src/hsforms.d.ts +75 -0
- package/dist/server/src/importHubspot.d.ts +1 -0
- package/dist/server/src/index.d.ts +12 -1
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -1246,6 +1246,13 @@ const contentTypes = {
|
|
|
1246
1246
|
submitLabel: localizedString(),
|
|
1247
1247
|
successMessage: localizedString("text"),
|
|
1248
1248
|
class: { type: "string" },
|
|
1249
|
+
// Marketing form GUID of the connected HubSpot portal. Test portal
|
|
1250
|
+
// today, production later — swap the private app + this GUID, no
|
|
1251
|
+
// code change. Empty = CRM upsert fallback (no native conversion).
|
|
1252
|
+
hubspotFormId: {
|
|
1253
|
+
type: "string",
|
|
1254
|
+
pluginOptions: { i18n: { localized: false } }
|
|
1255
|
+
},
|
|
1249
1256
|
definition: {
|
|
1250
1257
|
type: "json",
|
|
1251
1258
|
required: true,
|
|
@@ -1449,6 +1456,312 @@ function resolveSubmission(definition, rawValues) {
|
|
|
1449
1456
|
const unknown = Object.keys(rawValues).filter((key) => !declared.has(key));
|
|
1450
1457
|
return { values, hidden, missingRequired, unknown };
|
|
1451
1458
|
}
|
|
1459
|
+
const HS_BASE$2 = "https://api.hubapi.com";
|
|
1460
|
+
const OBJECT_BY_TYPE_ID = {
|
|
1461
|
+
"0-1": "contact",
|
|
1462
|
+
"0-2": "company"
|
|
1463
|
+
};
|
|
1464
|
+
const TYPE_MAP = {
|
|
1465
|
+
single_line_text: "text",
|
|
1466
|
+
multi_line_text: "textarea",
|
|
1467
|
+
email: "email",
|
|
1468
|
+
phone: "tel",
|
|
1469
|
+
mobile_phone: "tel",
|
|
1470
|
+
number: "number",
|
|
1471
|
+
dropdown: "select",
|
|
1472
|
+
select: "select",
|
|
1473
|
+
radio: "radio",
|
|
1474
|
+
multiple_checkboxes: "checkbox",
|
|
1475
|
+
single_checkbox: "checkbox",
|
|
1476
|
+
booleancheckbox: "checkbox"
|
|
1477
|
+
};
|
|
1478
|
+
let counter$1 = 0;
|
|
1479
|
+
const makeId$1 = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter$1 += 1).toString(36)}`;
|
|
1480
|
+
function convertCondition(parentFieldId, raw) {
|
|
1481
|
+
const operator = (raw.operator ?? "").toUpperCase();
|
|
1482
|
+
const values = (raw.values ?? []).map((v) => String(v)).filter((v) => v !== "");
|
|
1483
|
+
const spread = (op, logic) => values.length ? { logic, rules: values.map((value) => ({ field: parentFieldId, operator: op, value })) } : null;
|
|
1484
|
+
switch (operator) {
|
|
1485
|
+
case "EQ":
|
|
1486
|
+
case "SET_ANY":
|
|
1487
|
+
return spread("eq", "or");
|
|
1488
|
+
case "NEQ":
|
|
1489
|
+
case "NOT_SET_ANY":
|
|
1490
|
+
return spread("neq", "and");
|
|
1491
|
+
case "CONTAINS":
|
|
1492
|
+
return spread("contains", "or");
|
|
1493
|
+
case "GT":
|
|
1494
|
+
return spread("gt", "or");
|
|
1495
|
+
case "LT":
|
|
1496
|
+
return spread("lt", "or");
|
|
1497
|
+
case "SET":
|
|
1498
|
+
case "NOT_EMPTY":
|
|
1499
|
+
return { logic: "and", rules: [{ field: parentFieldId, operator: "notEmpty" }] };
|
|
1500
|
+
case "NOT_SET":
|
|
1501
|
+
case "EMPTY":
|
|
1502
|
+
return { logic: "and", rules: [{ field: parentFieldId, operator: "empty" }] };
|
|
1503
|
+
default:
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
function convertField(raw, ctx) {
|
|
1508
|
+
const label = raw.label || raw.name || "";
|
|
1509
|
+
const name = raw.name ?? "";
|
|
1510
|
+
if (raw.hidden) {
|
|
1511
|
+
ctx.skipped.push({ code: "hidden-field", label });
|
|
1512
|
+
return [];
|
|
1513
|
+
}
|
|
1514
|
+
const object = OBJECT_BY_TYPE_ID[raw.objectTypeId ?? "0-1"];
|
|
1515
|
+
if (!object) {
|
|
1516
|
+
ctx.skipped.push({ code: "object", label, detail: raw.objectTypeId });
|
|
1517
|
+
return [];
|
|
1518
|
+
}
|
|
1519
|
+
const type = TYPE_MAP[raw.fieldType ?? ""];
|
|
1520
|
+
if (!type || !name) {
|
|
1521
|
+
ctx.skipped.push({ code: "field-type", label, detail: raw.fieldType });
|
|
1522
|
+
return [];
|
|
1523
|
+
}
|
|
1524
|
+
if (ctx.usedNames.has(name)) {
|
|
1525
|
+
ctx.skipped.push({ code: "duplicate", label, detail: name });
|
|
1526
|
+
return [];
|
|
1527
|
+
}
|
|
1528
|
+
ctx.usedNames.add(name);
|
|
1529
|
+
const options = (raw.options ?? []).map((o) => ({ value: String(o.value ?? ""), label: o.label || void 0 })).filter((o) => o.value !== "");
|
|
1530
|
+
const field = {
|
|
1531
|
+
id: makeId$1("fld"),
|
|
1532
|
+
name,
|
|
1533
|
+
label,
|
|
1534
|
+
type,
|
|
1535
|
+
required: Boolean(raw.required),
|
|
1536
|
+
placeholder: raw.placeholder || void 0,
|
|
1537
|
+
helpText: raw.description || void 0,
|
|
1538
|
+
...options.length ? { options } : {},
|
|
1539
|
+
hubspot: { object, property: name },
|
|
1540
|
+
...ctx.parentCondition ? { visibleIf: ctx.parentCondition } : {}
|
|
1541
|
+
};
|
|
1542
|
+
const out = [field];
|
|
1543
|
+
for (const dependent of raw.dependentFields ?? []) {
|
|
1544
|
+
if (!dependent.field) continue;
|
|
1545
|
+
const childLabel = dependent.field.label || dependent.field.name || "";
|
|
1546
|
+
const condition = dependent.dependentCondition ? convertCondition(field.id, dependent.dependentCondition) : null;
|
|
1547
|
+
if (!condition) {
|
|
1548
|
+
ctx.skipped.push({
|
|
1549
|
+
code: "condition",
|
|
1550
|
+
label: childLabel,
|
|
1551
|
+
detail: dependent.dependentCondition?.operator
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
out.push(...convertField(dependent.field, { ...ctx, parentCondition: condition }));
|
|
1555
|
+
}
|
|
1556
|
+
return out;
|
|
1557
|
+
}
|
|
1558
|
+
function convertHubspotForm(raw) {
|
|
1559
|
+
const skipped = [];
|
|
1560
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
1561
|
+
const fields = [];
|
|
1562
|
+
for (const group of raw.fieldGroups ?? []) {
|
|
1563
|
+
if (group.richText && !(group.fields ?? []).length) {
|
|
1564
|
+
skipped.push({ code: "rich-text", detail: group.richText.slice(0, 80) });
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
const groupFields = (group.fields ?? []).flatMap(
|
|
1568
|
+
(f) => convertField(f, { usedNames, skipped })
|
|
1569
|
+
);
|
|
1570
|
+
const topLevel = (group.fields ?? []).length;
|
|
1571
|
+
if (topLevel === 2 && groupFields.length >= 2) {
|
|
1572
|
+
groupFields[0].width = "half";
|
|
1573
|
+
groupFields[1].width = "half";
|
|
1574
|
+
}
|
|
1575
|
+
fields.push(...groupFields);
|
|
1576
|
+
}
|
|
1577
|
+
if (raw.legalConsentOptions && Object.keys(raw.legalConsentOptions).length) {
|
|
1578
|
+
skipped.push({ code: "legal-consent" });
|
|
1579
|
+
}
|
|
1580
|
+
const postSubmit = raw.configuration?.postSubmitAction;
|
|
1581
|
+
return {
|
|
1582
|
+
name: raw.name ?? "",
|
|
1583
|
+
submitLabel: raw.displayOptions?.submitButtonText || null,
|
|
1584
|
+
successMessage: postSubmit?.type === "thank_you" && postSubmit.value ? postSubmit.value : null,
|
|
1585
|
+
hubspotFormId: raw.id ?? null,
|
|
1586
|
+
// HubSpot forms are single-page: one step, reorganizable in the builder.
|
|
1587
|
+
definition: { version: 1, steps: [{ id: makeId$1("stp"), fields }] },
|
|
1588
|
+
skipped
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
async function hsGet(apiKey, path) {
|
|
1592
|
+
const res = await fetch(`${HS_BASE$2}${path}`, {
|
|
1593
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1594
|
+
});
|
|
1595
|
+
if (!res.ok) {
|
|
1596
|
+
const body = await res.json().catch(() => ({}));
|
|
1597
|
+
throw Object.assign(new Error(body.message || `HubSpot ${res.status}`), {
|
|
1598
|
+
status: res.status
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
return await res.json();
|
|
1602
|
+
}
|
|
1603
|
+
async function listHubspotForms(apiKey) {
|
|
1604
|
+
const out = [];
|
|
1605
|
+
let after;
|
|
1606
|
+
for (let page = 0; page < 10; page += 1) {
|
|
1607
|
+
const query2 = after ? `&after=${encodeURIComponent(after)}` : "";
|
|
1608
|
+
const res = await hsGet(apiKey, `/marketing/v3/forms/?limit=100&formTypes=hubspot${query2}`);
|
|
1609
|
+
for (const form of res.results ?? []) {
|
|
1610
|
+
if (form.id && !form.archived) {
|
|
1611
|
+
out.push({ id: form.id, name: form.name || form.id, updatedAt: form.updatedAt });
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
after = res.paging?.next?.after;
|
|
1615
|
+
if (!after) break;
|
|
1616
|
+
}
|
|
1617
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1618
|
+
}
|
|
1619
|
+
async function fetchHubspotForm(apiKey, formId) {
|
|
1620
|
+
return hsGet(apiKey, `/marketing/v3/forms/${encodeURIComponent(formId)}`);
|
|
1621
|
+
}
|
|
1622
|
+
const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1623
|
+
const PORTAL = /^\d{1,12}$/;
|
|
1624
|
+
const HUTK = /^[a-f0-9]{8,128}$/i;
|
|
1625
|
+
const SHAPE_TTL_MS = 5 * 60 * 1e3;
|
|
1626
|
+
const DEFAULT_CONSENT_TEXT = "J'accepte que mes données soient utilisées pour me recontacter.";
|
|
1627
|
+
function isFormGuid(value) {
|
|
1628
|
+
return GUID.test(value.trim());
|
|
1629
|
+
}
|
|
1630
|
+
function sanitizeHutk(value) {
|
|
1631
|
+
if (typeof value !== "string") return void 0;
|
|
1632
|
+
const trimmed = value.trim();
|
|
1633
|
+
return HUTK.test(trimmed) ? trimmed : void 0;
|
|
1634
|
+
}
|
|
1635
|
+
function sanitizePortalId(value) {
|
|
1636
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
1637
|
+
const trimmed = String(value).trim();
|
|
1638
|
+
return PORTAL.test(trimmed) ? trimmed : void 0;
|
|
1639
|
+
}
|
|
1640
|
+
function sanitizeIp(value) {
|
|
1641
|
+
if (typeof value !== "string") return void 0;
|
|
1642
|
+
const trimmed = value.trim();
|
|
1643
|
+
return trimmed && trimmed.length <= 45 ? trimmed : void 0;
|
|
1644
|
+
}
|
|
1645
|
+
function formsHost(region) {
|
|
1646
|
+
const r = (region || "eu1").trim().toLowerCase();
|
|
1647
|
+
if (!r || r === "na1" || r === "na") return "api.hsforms.com";
|
|
1648
|
+
return `api-${r}.hsforms.com`;
|
|
1649
|
+
}
|
|
1650
|
+
function formsSubmitUrl(portalId, formGuid, region = "eu1") {
|
|
1651
|
+
return `https://${formsHost(region)}/submissions/v3/integration/secure/submit/${encodeURIComponent(portalId)}/${encodeURIComponent(formGuid)}`;
|
|
1652
|
+
}
|
|
1653
|
+
function toHsFields(props) {
|
|
1654
|
+
return Object.entries(props).filter(([, value]) => value !== "").map(([name, value]) => ({ name, value }));
|
|
1655
|
+
}
|
|
1656
|
+
function filterToFormFields(fields, allowed) {
|
|
1657
|
+
if (!allowed) return fields;
|
|
1658
|
+
return fields.filter((field) => allowed.has(field.name));
|
|
1659
|
+
}
|
|
1660
|
+
function formFieldNames(raw) {
|
|
1661
|
+
const names = [];
|
|
1662
|
+
for (const group of raw.fieldGroups ?? []) {
|
|
1663
|
+
for (const field of group.fields ?? []) {
|
|
1664
|
+
if (field.name) names.push(field.name);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return names;
|
|
1668
|
+
}
|
|
1669
|
+
const shapeCache = /* @__PURE__ */ new Map();
|
|
1670
|
+
async function loadFormShape(apiKey, formGuid) {
|
|
1671
|
+
const cached = shapeCache.get(formGuid);
|
|
1672
|
+
if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
|
|
1673
|
+
return { names: cached.names, hasLegalConsent: cached.hasLegalConsent };
|
|
1674
|
+
}
|
|
1675
|
+
try {
|
|
1676
|
+
const raw = await fetchHubspotForm(apiKey, formGuid);
|
|
1677
|
+
const names = new Set(formFieldNames(raw));
|
|
1678
|
+
const hasLegalConsent = Boolean(
|
|
1679
|
+
raw.legalConsentOptions && typeof raw.legalConsentOptions === "object" && raw.legalConsentOptions.type && raw.legalConsentOptions.type !== "none"
|
|
1680
|
+
);
|
|
1681
|
+
shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
|
|
1682
|
+
return { names, hasLegalConsent };
|
|
1683
|
+
} catch {
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
async function findContactIdByEmail(apiKey, email) {
|
|
1688
|
+
const res = await fetch("https://api.hubapi.com/crm/v3/objects/contacts/search", {
|
|
1689
|
+
method: "POST",
|
|
1690
|
+
headers: {
|
|
1691
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1692
|
+
"Content-Type": "application/json"
|
|
1693
|
+
},
|
|
1694
|
+
body: JSON.stringify({
|
|
1695
|
+
filterGroups: [
|
|
1696
|
+
{ filters: [{ propertyName: "email", operator: "EQ", value: email }] }
|
|
1697
|
+
],
|
|
1698
|
+
properties: ["email"],
|
|
1699
|
+
limit: 1
|
|
1700
|
+
})
|
|
1701
|
+
});
|
|
1702
|
+
const body = await res.json().catch(() => ({}));
|
|
1703
|
+
if (!res.ok) return void 0;
|
|
1704
|
+
return body.results?.[0]?.id;
|
|
1705
|
+
}
|
|
1706
|
+
function contextPayload(ctx) {
|
|
1707
|
+
const out = {};
|
|
1708
|
+
const hutk = sanitizeHutk(ctx?.hutk);
|
|
1709
|
+
if (hutk) out.hutk = hutk;
|
|
1710
|
+
if (typeof ctx?.pageUri === "string" && ctx.pageUri.trim()) {
|
|
1711
|
+
out.pageUri = ctx.pageUri.trim().slice(0, 1e3);
|
|
1712
|
+
}
|
|
1713
|
+
if (typeof ctx?.pageName === "string" && ctx.pageName.trim()) {
|
|
1714
|
+
out.pageName = ctx.pageName.trim().slice(0, 200);
|
|
1715
|
+
}
|
|
1716
|
+
const ip = sanitizeIp(ctx?.ipAddress);
|
|
1717
|
+
if (ip) out.ipAddress = ip;
|
|
1718
|
+
return out;
|
|
1719
|
+
}
|
|
1720
|
+
async function postForm(url, apiKey, payload) {
|
|
1721
|
+
const res = await fetch(url, {
|
|
1722
|
+
method: "POST",
|
|
1723
|
+
headers: {
|
|
1724
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1725
|
+
"Content-Type": "application/json"
|
|
1726
|
+
},
|
|
1727
|
+
body: JSON.stringify(payload)
|
|
1728
|
+
});
|
|
1729
|
+
if (res.ok) return { ok: true, status: res.status };
|
|
1730
|
+
const body = await res.json().catch(() => ({}));
|
|
1731
|
+
const error = body.errors?.[0]?.message || body.message || `HubSpot ${res.status}`;
|
|
1732
|
+
return { ok: false, status: res.status, error };
|
|
1733
|
+
}
|
|
1734
|
+
async function submitMarketingForm(input) {
|
|
1735
|
+
const portalId = sanitizePortalId(input.portalId);
|
|
1736
|
+
const formGuid = input.formGuid.trim();
|
|
1737
|
+
if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
|
|
1738
|
+
return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
|
|
1739
|
+
}
|
|
1740
|
+
const url = formsSubmitUrl(portalId, formGuid, input.region);
|
|
1741
|
+
const context = contextPayload(input.context);
|
|
1742
|
+
const base = {
|
|
1743
|
+
fields: input.fields,
|
|
1744
|
+
...Object.keys(context).length ? { context } : {}
|
|
1745
|
+
};
|
|
1746
|
+
const withConsent = input.consent?.given && input.formHasLegalConsent !== false ? {
|
|
1747
|
+
...base,
|
|
1748
|
+
legalConsentOptions: {
|
|
1749
|
+
consent: {
|
|
1750
|
+
consentToProcess: true,
|
|
1751
|
+
text: input.consent.text?.trim() || DEFAULT_CONSENT_TEXT
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
} : base;
|
|
1755
|
+
let result = await postForm(url, input.apiKey, withConsent);
|
|
1756
|
+
if (!result.ok && result.status === 400 && withConsent !== base) {
|
|
1757
|
+
result = await postForm(url, input.apiKey, base);
|
|
1758
|
+
}
|
|
1759
|
+
if (!result.ok && result.status === 400 && input.fields.length > 1) {
|
|
1760
|
+
const email = input.fields.find((f) => f.name === "email");
|
|
1761
|
+
if (email) result = await postForm(url, input.apiKey, { ...base, fields: [email] });
|
|
1762
|
+
}
|
|
1763
|
+
return result;
|
|
1764
|
+
}
|
|
1452
1765
|
function publicForm(entry) {
|
|
1453
1766
|
return {
|
|
1454
1767
|
slug: entry.slug,
|
|
@@ -1557,9 +1870,9 @@ function sanitizeRawValues(raw) {
|
|
|
1557
1870
|
}
|
|
1558
1871
|
return values;
|
|
1559
1872
|
}
|
|
1560
|
-
const HS_BASE$
|
|
1561
|
-
const HS_COMPANIES = `${HS_BASE$
|
|
1562
|
-
const HS_NOTES = `${HS_BASE$
|
|
1873
|
+
const HS_BASE$1 = "https://api.hubapi.com";
|
|
1874
|
+
const HS_COMPANIES = `${HS_BASE$1}/crm/v3/objects/companies`;
|
|
1875
|
+
const HS_NOTES = `${HS_BASE$1}/crm/v3/objects/notes`;
|
|
1563
1876
|
const ASSOC_NOTE_TO_CONTACT = 202;
|
|
1564
1877
|
const ASSOC_NOTE_TO_COMPANY = 190;
|
|
1565
1878
|
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1617,8 +1930,17 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1617
1930
|
const formsConfig = () => ({
|
|
1618
1931
|
companyFromDomain: true,
|
|
1619
1932
|
timelineNote: true,
|
|
1933
|
+
defaultFormId: "",
|
|
1620
1934
|
...strapi.plugin("hubspot").config("forms", {})
|
|
1621
1935
|
});
|
|
1936
|
+
const stringProps = (props) => {
|
|
1937
|
+
const out = {};
|
|
1938
|
+
for (const [key, value] of Object.entries(props)) {
|
|
1939
|
+
if (value === "" || value == null) continue;
|
|
1940
|
+
out[key] = typeof value === "boolean" ? value ? "true" : "false" : String(value);
|
|
1941
|
+
}
|
|
1942
|
+
return out;
|
|
1943
|
+
};
|
|
1622
1944
|
async function partitionBySchema(apiKey, groups) {
|
|
1623
1945
|
let schema;
|
|
1624
1946
|
try {
|
|
@@ -1658,20 +1980,20 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1658
1980
|
});
|
|
1659
1981
|
const existingId = found.results?.[0]?.id;
|
|
1660
1982
|
if (!existingId) return void 0;
|
|
1661
|
-
const
|
|
1662
|
-
for (const [key, value] of Object.entries(props))
|
|
1983
|
+
const stringProps2 = {};
|
|
1984
|
+
for (const [key, value] of Object.entries(props)) stringProps2[key] = String(value);
|
|
1663
1985
|
await hsJson(apiKey, `${HS_COMPANIES}/${existingId}`, {
|
|
1664
1986
|
method: "PATCH",
|
|
1665
|
-
body: { properties:
|
|
1987
|
+
body: { properties: stringProps2 }
|
|
1666
1988
|
});
|
|
1667
1989
|
return existingId;
|
|
1668
1990
|
}
|
|
1669
1991
|
async function createCompany(apiKey, props) {
|
|
1670
|
-
const
|
|
1671
|
-
for (const [key, value] of Object.entries(props))
|
|
1992
|
+
const stringProps2 = {};
|
|
1993
|
+
for (const [key, value] of Object.entries(props)) stringProps2[key] = String(value);
|
|
1672
1994
|
const created = await hsJson(apiKey, HS_COMPANIES, {
|
|
1673
1995
|
method: "POST",
|
|
1674
|
-
body: { properties:
|
|
1996
|
+
body: { properties: stringProps2 }
|
|
1675
1997
|
});
|
|
1676
1998
|
return created.id;
|
|
1677
1999
|
}
|
|
@@ -1684,19 +2006,19 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1684
2006
|
limit: 1
|
|
1685
2007
|
}
|
|
1686
2008
|
});
|
|
1687
|
-
const
|
|
1688
|
-
for (const [key, value] of Object.entries(props))
|
|
2009
|
+
const stringProps2 = { domain };
|
|
2010
|
+
for (const [key, value] of Object.entries(props)) stringProps2[key] = String(value);
|
|
1689
2011
|
const existingId = found.results?.[0]?.id;
|
|
1690
2012
|
if (existingId) {
|
|
1691
2013
|
await hsJson(apiKey, `${HS_COMPANIES}/${existingId}`, {
|
|
1692
2014
|
method: "PATCH",
|
|
1693
|
-
body: { properties:
|
|
2015
|
+
body: { properties: stringProps2 }
|
|
1694
2016
|
});
|
|
1695
2017
|
return existingId;
|
|
1696
2018
|
}
|
|
1697
2019
|
const created = await hsJson(apiKey, HS_COMPANIES, {
|
|
1698
2020
|
method: "POST",
|
|
1699
|
-
body: { properties:
|
|
2021
|
+
body: { properties: stringProps2 }
|
|
1700
2022
|
});
|
|
1701
2023
|
return created.id;
|
|
1702
2024
|
}
|
|
@@ -1806,6 +2128,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1806
2128
|
});
|
|
1807
2129
|
}
|
|
1808
2130
|
}
|
|
2131
|
+
let usedFormsApi = false;
|
|
1809
2132
|
if (email && apiKey) {
|
|
1810
2133
|
const groups = groupByObject(form.definition, resolution.values);
|
|
1811
2134
|
for (const [object, props] of Object.entries(companyGroups)) {
|
|
@@ -1813,14 +2136,62 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1813
2136
|
}
|
|
1814
2137
|
const partition = await partitionBySchema(apiKey, groups);
|
|
1815
2138
|
rejected = partition.rejected;
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2139
|
+
const portalId = sanitizePortalId(strapi.plugin("hubspot").config("portalId", ""));
|
|
2140
|
+
const region = String(strapi.plugin("hubspot").config("region", "eu1") || "eu1");
|
|
2141
|
+
const ownGuid = typeof form.hubspotFormId === "string" ? form.hubspotFormId.trim() : "";
|
|
2142
|
+
const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(config2.defaultFormId) ? config2.defaultFormId.trim() : "";
|
|
2143
|
+
if (portalId && formGuid) {
|
|
2144
|
+
try {
|
|
2145
|
+
const shape = await loadFormShape(apiKey, formGuid);
|
|
2146
|
+
const fields = filterToFormFields(
|
|
2147
|
+
toHsFields({ ...stringProps(partition.accepted.contact ?? {}), email }),
|
|
2148
|
+
shape?.names ?? null
|
|
2149
|
+
);
|
|
2150
|
+
if (fields.length && !fields.some((f) => f.name === "email")) {
|
|
2151
|
+
fields.unshift({ name: "email", value: email });
|
|
2152
|
+
}
|
|
2153
|
+
const submitted = await submitMarketingForm({
|
|
2154
|
+
apiKey,
|
|
2155
|
+
portalId,
|
|
2156
|
+
formGuid,
|
|
2157
|
+
region,
|
|
2158
|
+
fields: fields.length ? fields : [{ name: "email", value: email }],
|
|
2159
|
+
context: {
|
|
2160
|
+
hutk: sanitizeHutk(meta.hutk),
|
|
2161
|
+
pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
|
|
2162
|
+
pageName: typeof meta.pageName === "string" && meta.pageName || (form.title || form.name) || void 0,
|
|
2163
|
+
ipAddress: sanitizeIp(meta.ipAddress)
|
|
2164
|
+
},
|
|
2165
|
+
consent: { given: meta.consent === true },
|
|
2166
|
+
formHasLegalConsent: shape?.hasLegalConsent
|
|
2167
|
+
});
|
|
2168
|
+
if (submitted.ok) {
|
|
2169
|
+
hubspotSynced = true;
|
|
2170
|
+
usedFormsApi = true;
|
|
2171
|
+
try {
|
|
2172
|
+
contactId = await findContactIdByEmail(apiKey, email);
|
|
2173
|
+
} catch (err) {
|
|
2174
|
+
strapi.log.warn(
|
|
2175
|
+
`[hubspot] contact lookup after form submit failed — ${err.message}`
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
} else {
|
|
2179
|
+
strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
|
|
2180
|
+
}
|
|
2181
|
+
} catch (err) {
|
|
2182
|
+
strapi.log.warn(`[hubspot] Forms API submit failed — ${err.message}`);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
if (!usedFormsApi) {
|
|
2186
|
+
const result = await strapi.plugin("hubspot").service("submit").upsert({
|
|
2187
|
+
object: "contact",
|
|
2188
|
+
idProperty: "email",
|
|
2189
|
+
properties: { ...partition.accepted.contact ?? {}, email }
|
|
2190
|
+
});
|
|
2191
|
+
if (result.ok) {
|
|
2192
|
+
hubspotSynced = true;
|
|
2193
|
+
contactId = result.id;
|
|
2194
|
+
}
|
|
1824
2195
|
}
|
|
1825
2196
|
const domain = corporateDomain(email);
|
|
1826
2197
|
const companyProps = partition.accepted.company ?? {};
|
|
@@ -1844,7 +2215,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1844
2215
|
if (companyId) {
|
|
1845
2216
|
await hsJson(
|
|
1846
2217
|
apiKey,
|
|
1847
|
-
`${HS_BASE$
|
|
2218
|
+
`${HS_BASE$1}/crm/v4/objects/contacts/${contactId}/associations/default/companies/${companyId}`,
|
|
1848
2219
|
{ method: "PUT" }
|
|
1849
2220
|
);
|
|
1850
2221
|
}
|
|
@@ -1852,7 +2223,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1852
2223
|
strapi.log.warn(`[hubspot] company sync failed (contact OK) — ${err.message}`);
|
|
1853
2224
|
}
|
|
1854
2225
|
}
|
|
1855
|
-
if (contactId && config2.timelineNote) {
|
|
2226
|
+
if (contactId && config2.timelineNote && !usedFormsApi) {
|
|
1856
2227
|
try {
|
|
1857
2228
|
await createLeadNote(apiKey, {
|
|
1858
2229
|
contactId,
|
|
@@ -1887,168 +2258,6 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1887
2258
|
}
|
|
1888
2259
|
return { submit };
|
|
1889
2260
|
}
|
|
1890
|
-
const HS_BASE$1 = "https://api.hubapi.com";
|
|
1891
|
-
const OBJECT_BY_TYPE_ID = {
|
|
1892
|
-
"0-1": "contact",
|
|
1893
|
-
"0-2": "company"
|
|
1894
|
-
};
|
|
1895
|
-
const TYPE_MAP = {
|
|
1896
|
-
single_line_text: "text",
|
|
1897
|
-
multi_line_text: "textarea",
|
|
1898
|
-
email: "email",
|
|
1899
|
-
phone: "tel",
|
|
1900
|
-
mobile_phone: "tel",
|
|
1901
|
-
number: "number",
|
|
1902
|
-
dropdown: "select",
|
|
1903
|
-
select: "select",
|
|
1904
|
-
radio: "radio",
|
|
1905
|
-
multiple_checkboxes: "checkbox",
|
|
1906
|
-
single_checkbox: "checkbox",
|
|
1907
|
-
booleancheckbox: "checkbox"
|
|
1908
|
-
};
|
|
1909
|
-
let counter$1 = 0;
|
|
1910
|
-
const makeId$1 = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter$1 += 1).toString(36)}`;
|
|
1911
|
-
function convertCondition(parentFieldId, raw) {
|
|
1912
|
-
const operator = (raw.operator ?? "").toUpperCase();
|
|
1913
|
-
const values = (raw.values ?? []).map((v) => String(v)).filter((v) => v !== "");
|
|
1914
|
-
const spread = (op, logic) => values.length ? { logic, rules: values.map((value) => ({ field: parentFieldId, operator: op, value })) } : null;
|
|
1915
|
-
switch (operator) {
|
|
1916
|
-
case "EQ":
|
|
1917
|
-
case "SET_ANY":
|
|
1918
|
-
return spread("eq", "or");
|
|
1919
|
-
case "NEQ":
|
|
1920
|
-
case "NOT_SET_ANY":
|
|
1921
|
-
return spread("neq", "and");
|
|
1922
|
-
case "CONTAINS":
|
|
1923
|
-
return spread("contains", "or");
|
|
1924
|
-
case "GT":
|
|
1925
|
-
return spread("gt", "or");
|
|
1926
|
-
case "LT":
|
|
1927
|
-
return spread("lt", "or");
|
|
1928
|
-
case "SET":
|
|
1929
|
-
case "NOT_EMPTY":
|
|
1930
|
-
return { logic: "and", rules: [{ field: parentFieldId, operator: "notEmpty" }] };
|
|
1931
|
-
case "NOT_SET":
|
|
1932
|
-
case "EMPTY":
|
|
1933
|
-
return { logic: "and", rules: [{ field: parentFieldId, operator: "empty" }] };
|
|
1934
|
-
default:
|
|
1935
|
-
return null;
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
function convertField(raw, ctx) {
|
|
1939
|
-
const label = raw.label || raw.name || "";
|
|
1940
|
-
const name = raw.name ?? "";
|
|
1941
|
-
if (raw.hidden) {
|
|
1942
|
-
ctx.skipped.push({ code: "hidden-field", label });
|
|
1943
|
-
return [];
|
|
1944
|
-
}
|
|
1945
|
-
const object = OBJECT_BY_TYPE_ID[raw.objectTypeId ?? "0-1"];
|
|
1946
|
-
if (!object) {
|
|
1947
|
-
ctx.skipped.push({ code: "object", label, detail: raw.objectTypeId });
|
|
1948
|
-
return [];
|
|
1949
|
-
}
|
|
1950
|
-
const type = TYPE_MAP[raw.fieldType ?? ""];
|
|
1951
|
-
if (!type || !name) {
|
|
1952
|
-
ctx.skipped.push({ code: "field-type", label, detail: raw.fieldType });
|
|
1953
|
-
return [];
|
|
1954
|
-
}
|
|
1955
|
-
if (ctx.usedNames.has(name)) {
|
|
1956
|
-
ctx.skipped.push({ code: "duplicate", label, detail: name });
|
|
1957
|
-
return [];
|
|
1958
|
-
}
|
|
1959
|
-
ctx.usedNames.add(name);
|
|
1960
|
-
const options = (raw.options ?? []).map((o) => ({ value: String(o.value ?? ""), label: o.label || void 0 })).filter((o) => o.value !== "");
|
|
1961
|
-
const field = {
|
|
1962
|
-
id: makeId$1("fld"),
|
|
1963
|
-
name,
|
|
1964
|
-
label,
|
|
1965
|
-
type,
|
|
1966
|
-
required: Boolean(raw.required),
|
|
1967
|
-
placeholder: raw.placeholder || void 0,
|
|
1968
|
-
helpText: raw.description || void 0,
|
|
1969
|
-
...options.length ? { options } : {},
|
|
1970
|
-
hubspot: { object, property: name },
|
|
1971
|
-
...ctx.parentCondition ? { visibleIf: ctx.parentCondition } : {}
|
|
1972
|
-
};
|
|
1973
|
-
const out = [field];
|
|
1974
|
-
for (const dependent of raw.dependentFields ?? []) {
|
|
1975
|
-
if (!dependent.field) continue;
|
|
1976
|
-
const childLabel = dependent.field.label || dependent.field.name || "";
|
|
1977
|
-
const condition = dependent.dependentCondition ? convertCondition(field.id, dependent.dependentCondition) : null;
|
|
1978
|
-
if (!condition) {
|
|
1979
|
-
ctx.skipped.push({
|
|
1980
|
-
code: "condition",
|
|
1981
|
-
label: childLabel,
|
|
1982
|
-
detail: dependent.dependentCondition?.operator
|
|
1983
|
-
});
|
|
1984
|
-
}
|
|
1985
|
-
out.push(...convertField(dependent.field, { ...ctx, parentCondition: condition }));
|
|
1986
|
-
}
|
|
1987
|
-
return out;
|
|
1988
|
-
}
|
|
1989
|
-
function convertHubspotForm(raw) {
|
|
1990
|
-
const skipped = [];
|
|
1991
|
-
const usedNames = /* @__PURE__ */ new Set();
|
|
1992
|
-
const fields = [];
|
|
1993
|
-
for (const group of raw.fieldGroups ?? []) {
|
|
1994
|
-
if (group.richText && !(group.fields ?? []).length) {
|
|
1995
|
-
skipped.push({ code: "rich-text", detail: group.richText.slice(0, 80) });
|
|
1996
|
-
continue;
|
|
1997
|
-
}
|
|
1998
|
-
const groupFields = (group.fields ?? []).flatMap(
|
|
1999
|
-
(f) => convertField(f, { usedNames, skipped })
|
|
2000
|
-
);
|
|
2001
|
-
const topLevel = (group.fields ?? []).length;
|
|
2002
|
-
if (topLevel === 2 && groupFields.length >= 2) {
|
|
2003
|
-
groupFields[0].width = "half";
|
|
2004
|
-
groupFields[1].width = "half";
|
|
2005
|
-
}
|
|
2006
|
-
fields.push(...groupFields);
|
|
2007
|
-
}
|
|
2008
|
-
if (raw.legalConsentOptions && Object.keys(raw.legalConsentOptions).length) {
|
|
2009
|
-
skipped.push({ code: "legal-consent" });
|
|
2010
|
-
}
|
|
2011
|
-
const postSubmit = raw.configuration?.postSubmitAction;
|
|
2012
|
-
return {
|
|
2013
|
-
name: raw.name ?? "",
|
|
2014
|
-
submitLabel: raw.displayOptions?.submitButtonText || null,
|
|
2015
|
-
successMessage: postSubmit?.type === "thank_you" && postSubmit.value ? postSubmit.value : null,
|
|
2016
|
-
// HubSpot forms are single-page: one step, reorganizable in the builder.
|
|
2017
|
-
definition: { version: 1, steps: [{ id: makeId$1("stp"), fields }] },
|
|
2018
|
-
skipped
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
|
-
async function hsGet(apiKey, path) {
|
|
2022
|
-
const res = await fetch(`${HS_BASE$1}${path}`, {
|
|
2023
|
-
headers: { Authorization: `Bearer ${apiKey}` }
|
|
2024
|
-
});
|
|
2025
|
-
if (!res.ok) {
|
|
2026
|
-
const body = await res.json().catch(() => ({}));
|
|
2027
|
-
throw Object.assign(new Error(body.message || `HubSpot ${res.status}`), {
|
|
2028
|
-
status: res.status
|
|
2029
|
-
});
|
|
2030
|
-
}
|
|
2031
|
-
return await res.json();
|
|
2032
|
-
}
|
|
2033
|
-
async function listHubspotForms(apiKey) {
|
|
2034
|
-
const out = [];
|
|
2035
|
-
let after;
|
|
2036
|
-
for (let page = 0; page < 10; page += 1) {
|
|
2037
|
-
const query2 = after ? `&after=${encodeURIComponent(after)}` : "";
|
|
2038
|
-
const res = await hsGet(apiKey, `/marketing/v3/forms/?limit=100&formTypes=hubspot${query2}`);
|
|
2039
|
-
for (const form of res.results ?? []) {
|
|
2040
|
-
if (form.id && !form.archived) {
|
|
2041
|
-
out.push({ id: form.id, name: form.name || form.id, updatedAt: form.updatedAt });
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
after = res.paging?.next?.after;
|
|
2045
|
-
if (!after) break;
|
|
2046
|
-
}
|
|
2047
|
-
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
2048
|
-
}
|
|
2049
|
-
async function fetchHubspotForm(apiKey, formId) {
|
|
2050
|
-
return hsGet(apiKey, `/marketing/v3/forms/${encodeURIComponent(formId)}`);
|
|
2051
|
-
}
|
|
2052
2261
|
let counter = 0;
|
|
2053
2262
|
const makeId = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter += 1).toString(36)}`;
|
|
2054
2263
|
const slugifyName = (label) => label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
@@ -2127,7 +2336,8 @@ const WRITABLE = [
|
|
|
2127
2336
|
"nextLabel",
|
|
2128
2337
|
"submitLabel",
|
|
2129
2338
|
"successMessage",
|
|
2130
|
-
"class"
|
|
2339
|
+
"class",
|
|
2340
|
+
"hubspotFormId"
|
|
2131
2341
|
];
|
|
2132
2342
|
const slugify = (value) => value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "form";
|
|
2133
2343
|
function createFormsAdminController(strapi) {
|
|
@@ -2492,6 +2702,7 @@ function createFormsAdminController(strapi) {
|
|
|
2492
2702
|
submitLabel: source.submitLabel,
|
|
2493
2703
|
successMessage: source.successMessage,
|
|
2494
2704
|
class: source.class,
|
|
2705
|
+
hubspotFormId: source.hubspotFormId ?? null,
|
|
2495
2706
|
definition: source.definition
|
|
2496
2707
|
};
|
|
2497
2708
|
if (targetId) {
|
|
@@ -2726,17 +2937,27 @@ const FORMS_ACTION = "plugin::hubspot.forms";
|
|
|
2726
2937
|
const config = {
|
|
2727
2938
|
default: {
|
|
2728
2939
|
apiKey: "",
|
|
2940
|
+
// HubSpot account that receives form submissions. A test portal id is
|
|
2941
|
+
// fine in development; production swaps this env var, not the code.
|
|
2942
|
+
portalId: "",
|
|
2943
|
+
// Hosting region of that portal (`eu1`, `na1`, …). Drives the Forms API
|
|
2944
|
+
// host (`api-eu1.hsforms.com`) — CRM stays on api.hubapi.com.
|
|
2945
|
+
region: "eu1",
|
|
2729
2946
|
// Objects whose properties are offered. Names from the standard set, or
|
|
2730
2947
|
// `{ name, path }` for a custom object type.
|
|
2731
2948
|
objects: ["contact", "company"],
|
|
2732
2949
|
validate: [],
|
|
2733
|
-
// Submission pipeline of the built forms
|
|
2950
|
+
// Submission pipeline of the built forms:
|
|
2734
2951
|
// - companyFromDomain: a corporate email upserts the Company (deduped by
|
|
2735
2952
|
// domain) and associates it to the contact;
|
|
2736
|
-
// - timelineNote:
|
|
2953
|
+
// - timelineNote: recap note on the CRM-upsert fallback only (off when
|
|
2954
|
+
// submissions go through the marketing Forms API);
|
|
2955
|
+
// - defaultFormId: portal-wide marketing form GUID when a builder form
|
|
2956
|
+
// doesn't set its own — swap with the portal at production cutover.
|
|
2737
2957
|
forms: {
|
|
2738
2958
|
companyFromDomain: true,
|
|
2739
2959
|
timelineNote: true,
|
|
2960
|
+
defaultFormId: "",
|
|
2740
2961
|
// Per-IP brakes on the public routes; 0 disables one.
|
|
2741
2962
|
rateLimit: { submitPerMinute: 6, searchPerMinute: 30 }
|
|
2742
2963
|
}
|
|
@@ -2892,7 +3113,7 @@ const controllers = {
|
|
|
2892
3113
|
if (!values || !Object.keys(values).length) ctx.throw(422, "Invalid submission");
|
|
2893
3114
|
const rawMeta = ctx.request.body?.meta ?? {};
|
|
2894
3115
|
const meta = {};
|
|
2895
|
-
for (const key of ["pagePath", "pageUrl", "originPath", "originLabel", "source"]) {
|
|
3116
|
+
for (const key of ["pagePath", "pageUrl", "pageName", "originPath", "originLabel", "source", "ipAddress"]) {
|
|
2896
3117
|
const v = rawMeta[key];
|
|
2897
3118
|
if (typeof v === "string" && v) meta[key] = v.slice(0, 500);
|
|
2898
3119
|
}
|
|
@@ -2900,6 +3121,13 @@ const controllers = {
|
|
|
2900
3121
|
if (typeof rawMeta.consentedAt === "string" && rawMeta.consentedAt.trim()) {
|
|
2901
3122
|
meta.consentedAt = rawMeta.consentedAt.trim().slice(0, 40);
|
|
2902
3123
|
}
|
|
3124
|
+
if (typeof rawMeta.hutk === "string" && rawMeta.hutk.trim()) {
|
|
3125
|
+
meta.hutk = rawMeta.hutk.trim().slice(0, 128);
|
|
3126
|
+
}
|
|
3127
|
+
if (!meta.ipAddress) {
|
|
3128
|
+
const ip = ctx.request.ip;
|
|
3129
|
+
if (ip) meta.ipAddress = ip.slice(0, 45);
|
|
3130
|
+
}
|
|
2903
3131
|
const outcome = await strapi.plugin("hubspot").service("forms").submit(entry, values, meta);
|
|
2904
3132
|
if (!outcome.ok) {
|
|
2905
3133
|
ctx.status = 422;
|