strapi-plugin-hubspot 0.15.0 → 0.16.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 +14 -0
- package/README.md +28 -13
- package/dist/_chunks/{Forms-B7eIm0Fi.mjs → Forms-6EoEYzAc.mjs} +26 -5
- package/dist/_chunks/{Forms-ClyI4MJ1.js → Forms-BW_zGc1P.js} +26 -5
- package/dist/_chunks/{HubspotFormInput-cZAiaAaq.mjs → HubspotFormInput-V5w4y1fb.mjs} +1 -1
- package/dist/_chunks/{HubspotFormInput-Do7F83V6.js → HubspotFormInput-oMhm1UJm.js} +1 -1
- package/dist/_chunks/{HubspotObjectInput-DWqk2C26.js → HubspotObjectInput-Bmo6YQgs.js} +3 -3
- package/dist/_chunks/{HubspotObjectInput-ZZk_7hWq.mjs → HubspotObjectInput-DhsnSguG.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-5bkDGmGo.mjs → HubspotPropertyInput-CrYP4pc0.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-v9B3wb1a.js → HubspotPropertyInput-luFfnxqy.js} +3 -3
- package/dist/_chunks/{Settings-X3EHnTaO.js → Settings-B3A22i7Z.js} +77 -4
- package/dist/_chunks/{Settings-QcFbm3s5.mjs → Settings-CjcWj9I6.mjs} +78 -5
- package/dist/_chunks/{en-ZHvaZLi7.js → en-C7ls2EUl.js} +15 -2
- package/dist/_chunks/{en-DMJeF7EN.mjs → en-D6GHgMOt.mjs} +15 -2
- package/dist/_chunks/{fr-CVAntLmc.mjs → fr-CfohMY8n.mjs} +15 -2
- package/dist/_chunks/{fr-Ds_2EAzZ.js → fr-pOP8ZXqM.js} +15 -2
- package/dist/_chunks/{index-BSJAhj8r.mjs → index-7cL9PRh1.mjs} +6 -6
- package/dist/_chunks/{index-BpcilDy1.js → index-CodpLKZx.js} +6 -6
- package/dist/_chunks/{objectLabels-CYXXFpNB.js → objectLabels-CSWGIgNb.js} +1 -1
- package/dist/_chunks/{objectLabels-CrYRzsqe.mjs → objectLabels-Dnj14fsp.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-CBF4GkmK.mjs → useHubspotSchema-BsmQgJgx.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-Ba6xZUCL.js → useHubspotSchema-DiRN3HrN.js} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +216 -7
- package/dist/server/index.mjs +216 -7
- package/dist/server/src/__tests__/formSync.test.d.ts +1 -0
- package/dist/server/src/formSync.d.ts +37 -0
- package/dist/server/src/forms.d.ts +6 -8
- package/dist/server/src/hsforms.d.ts +8 -0
- package/dist/server/src/importHubspot.d.ts +4 -0
- package/dist/server/src/index.d.ts +6 -0
- package/dist/server/src/settings.d.ts +32 -3
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -176,6 +176,26 @@ const first = (values, fallback = "") => {
|
|
|
176
176
|
}
|
|
177
177
|
return { value: fallback, source: null };
|
|
178
178
|
};
|
|
179
|
+
const MODES = /* @__PURE__ */ new Set(["auto", "crm", "forms"]);
|
|
180
|
+
function asSubmissionMode(value) {
|
|
181
|
+
return typeof value === "string" && MODES.has(value) ? value : void 0;
|
|
182
|
+
}
|
|
183
|
+
function asBool(value) {
|
|
184
|
+
return typeof value === "boolean" ? value : void 0;
|
|
185
|
+
}
|
|
186
|
+
function formsCfg(strapi) {
|
|
187
|
+
return strapi.plugin("hubspot").config("forms", {}) ?? {};
|
|
188
|
+
}
|
|
189
|
+
function policyFrom(stored, forms) {
|
|
190
|
+
return {
|
|
191
|
+
submissionMode: asSubmissionMode(stored.submissionMode) ?? asSubmissionMode(forms.submissionMode) ?? "auto",
|
|
192
|
+
writeExtraProperties: asBool(stored.writeExtraProperties) ?? asBool(forms.writeExtraProperties) ?? true,
|
|
193
|
+
syncFieldsOnPublish: asBool(stored.syncFieldsOnPublish) ?? asBool(forms.syncFieldsOnPublish) ?? false
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function resolvePolicy(strapi) {
|
|
197
|
+
return policyFrom(await getStoredSettings(strapi), formsCfg(strapi));
|
|
198
|
+
}
|
|
179
199
|
async function resolveApiKey(strapi) {
|
|
180
200
|
const stored = await getStoredSettings(strapi);
|
|
181
201
|
if (stored.apiKey) return { apiKey: stored.apiKey, source: "settings" };
|
|
@@ -195,7 +215,7 @@ async function resolveAccount(strapi) {
|
|
|
195
215
|
}
|
|
196
216
|
async function resolveAccountSources(strapi) {
|
|
197
217
|
const stored = await getStoredSettings(strapi);
|
|
198
|
-
const
|
|
218
|
+
const forms = formsCfg(strapi);
|
|
199
219
|
const portal = first([
|
|
200
220
|
{ value: stored.portalId, source: "settings" },
|
|
201
221
|
{ value: strapi.plugin("hubspot").config("portalId", ""), source: "config" },
|
|
@@ -211,10 +231,11 @@ async function resolveAccountSources(strapi) {
|
|
|
211
231
|
);
|
|
212
232
|
const form = first([
|
|
213
233
|
{ value: stored.defaultFormId, source: "settings" },
|
|
214
|
-
{ value:
|
|
234
|
+
{ value: forms.defaultFormId, source: "config" },
|
|
215
235
|
{ value: process.env[ENV_DEFAULT_FORM], source: "env" }
|
|
216
236
|
]);
|
|
217
237
|
const { apiKey, source: keySource } = await resolveApiKey(strapi);
|
|
238
|
+
const policy = policyFrom(stored, forms);
|
|
218
239
|
return {
|
|
219
240
|
configured: Boolean(apiKey),
|
|
220
241
|
keySource,
|
|
@@ -224,7 +245,8 @@ async function resolveAccountSources(strapi) {
|
|
|
224
245
|
defaultFormId: form.value,
|
|
225
246
|
portalSource: portal.source,
|
|
226
247
|
regionSource: region.source,
|
|
227
|
-
formSource: form.source
|
|
248
|
+
formSource: form.source,
|
|
249
|
+
...policy
|
|
228
250
|
};
|
|
229
251
|
}
|
|
230
252
|
async function publicSettings(strapi) {
|
|
@@ -242,6 +264,16 @@ async function patchStoredSettings(strapi, body) {
|
|
|
242
264
|
next.region = region || "eu1";
|
|
243
265
|
}
|
|
244
266
|
if ("defaultFormId" in body) next.defaultFormId = String(body.defaultFormId ?? "").trim();
|
|
267
|
+
if ("submissionMode" in body) {
|
|
268
|
+
const mode = asSubmissionMode(body.submissionMode);
|
|
269
|
+
if (mode) next.submissionMode = mode;
|
|
270
|
+
}
|
|
271
|
+
if ("writeExtraProperties" in body && typeof body.writeExtraProperties === "boolean") {
|
|
272
|
+
next.writeExtraProperties = body.writeExtraProperties;
|
|
273
|
+
}
|
|
274
|
+
if ("syncFieldsOnPublish" in body && typeof body.syncFieldsOnPublish === "boolean") {
|
|
275
|
+
next.syncFieldsOnPublish = body.syncFieldsOnPublish;
|
|
276
|
+
}
|
|
245
277
|
await setStoredSettings(strapi, next);
|
|
246
278
|
}
|
|
247
279
|
async function clearStoredApiKey(strapi) {
|
|
@@ -1523,6 +1555,96 @@ function resolveSubmission(definition, rawValues) {
|
|
|
1523
1555
|
const unknown = Object.keys(rawValues).filter((key) => !declared.has(key));
|
|
1524
1556
|
return { values, hidden, missingRequired, unknown };
|
|
1525
1557
|
}
|
|
1558
|
+
const OBJECT_TYPE_ID = {
|
|
1559
|
+
contact: "0-1",
|
|
1560
|
+
company: "0-2"
|
|
1561
|
+
};
|
|
1562
|
+
function leftoverContactProps(contact, sentFieldNames) {
|
|
1563
|
+
const sent = new Set(sentFieldNames);
|
|
1564
|
+
const extra = {};
|
|
1565
|
+
for (const [key, value] of Object.entries(contact)) {
|
|
1566
|
+
if (key === "email" || sent.has(key) || value === "") continue;
|
|
1567
|
+
extra[key] = value;
|
|
1568
|
+
}
|
|
1569
|
+
return extra;
|
|
1570
|
+
}
|
|
1571
|
+
function mappedContactFields(definition) {
|
|
1572
|
+
const out = [];
|
|
1573
|
+
for (const step of definition.steps ?? []) {
|
|
1574
|
+
for (const fld of step.fields ?? []) {
|
|
1575
|
+
if (fld.type === "company") continue;
|
|
1576
|
+
const mapping = fld.hubspot ?? {};
|
|
1577
|
+
const object = mapping.object?.trim() || "contact";
|
|
1578
|
+
if (object !== "contact") continue;
|
|
1579
|
+
const property = mapping.property?.trim() || fld.name;
|
|
1580
|
+
if (!property) continue;
|
|
1581
|
+
out.push({
|
|
1582
|
+
name: fld.name,
|
|
1583
|
+
label: typeof fld.label === "string" ? fld.label : void 0,
|
|
1584
|
+
type: fld.type,
|
|
1585
|
+
required: Boolean(fld.required),
|
|
1586
|
+
object,
|
|
1587
|
+
property,
|
|
1588
|
+
options: fld.options
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return out;
|
|
1593
|
+
}
|
|
1594
|
+
function missingMappedFields(definition, existingNames) {
|
|
1595
|
+
const existing = new Set(existingNames);
|
|
1596
|
+
return mappedContactFields(definition).filter((field) => !existing.has(field.property));
|
|
1597
|
+
}
|
|
1598
|
+
function hsFieldType(type, options) {
|
|
1599
|
+
switch (type) {
|
|
1600
|
+
case "email":
|
|
1601
|
+
return "email";
|
|
1602
|
+
case "tel":
|
|
1603
|
+
return "phone";
|
|
1604
|
+
case "textarea":
|
|
1605
|
+
return "multi_line_text";
|
|
1606
|
+
case "number":
|
|
1607
|
+
return "number";
|
|
1608
|
+
case "select":
|
|
1609
|
+
return "dropdown";
|
|
1610
|
+
case "radio":
|
|
1611
|
+
return "radio";
|
|
1612
|
+
case "checkbox":
|
|
1613
|
+
return Array.isArray(options) && options.length ? "multiple_checkboxes" : "booleancheckbox";
|
|
1614
|
+
default:
|
|
1615
|
+
return "single_line_text";
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function toHsFormField(field) {
|
|
1619
|
+
const options = (field.options ?? []).filter((option) => option.value).map((option) => ({
|
|
1620
|
+
value: String(option.value),
|
|
1621
|
+
label: option.label || String(option.value)
|
|
1622
|
+
}));
|
|
1623
|
+
return {
|
|
1624
|
+
objectTypeId: OBJECT_TYPE_ID[field.object || "contact"] || "0-1",
|
|
1625
|
+
name: field.property,
|
|
1626
|
+
label: field.label || field.property,
|
|
1627
|
+
required: Boolean(field.required),
|
|
1628
|
+
hidden: false,
|
|
1629
|
+
fieldType: hsFieldType(field.type, field.options),
|
|
1630
|
+
...options.length ? { options } : {}
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function appendFieldGroups(existing, extra) {
|
|
1634
|
+
return [
|
|
1635
|
+
...existing.map((group) => ({
|
|
1636
|
+
groupType: group.groupType || "default_group",
|
|
1637
|
+
richTextType: group.richTextType || "text",
|
|
1638
|
+
...group.richText ? { richText: group.richText } : {},
|
|
1639
|
+
fields: group.fields ?? []
|
|
1640
|
+
})),
|
|
1641
|
+
...extra.map((field) => ({
|
|
1642
|
+
groupType: "default_group",
|
|
1643
|
+
richTextType: "text",
|
|
1644
|
+
fields: [toHsFormField(field)]
|
|
1645
|
+
}))
|
|
1646
|
+
];
|
|
1647
|
+
}
|
|
1526
1648
|
const HS_BASE$2 = "https://api.hubapi.com";
|
|
1527
1649
|
const OBJECT_BY_TYPE_ID = {
|
|
1528
1650
|
"0-1": "contact",
|
|
@@ -1686,6 +1808,23 @@ async function listHubspotForms(apiKey) {
|
|
|
1686
1808
|
async function fetchHubspotForm(apiKey, formId) {
|
|
1687
1809
|
return hsGet(apiKey, `/marketing/v3/forms/${encodeURIComponent(formId)}`);
|
|
1688
1810
|
}
|
|
1811
|
+
async function patchHubspotForm(apiKey, formId, body) {
|
|
1812
|
+
const res = await fetch(`${HS_BASE$2}/marketing/v3/forms/${encodeURIComponent(formId)}`, {
|
|
1813
|
+
method: "PATCH",
|
|
1814
|
+
headers: {
|
|
1815
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1816
|
+
"Content-Type": "application/json"
|
|
1817
|
+
},
|
|
1818
|
+
body: JSON.stringify(body)
|
|
1819
|
+
});
|
|
1820
|
+
if (!res.ok) {
|
|
1821
|
+
const payload = await res.json().catch(() => ({}));
|
|
1822
|
+
throw Object.assign(new Error(payload.message || `HubSpot ${res.status}`), {
|
|
1823
|
+
status: res.status
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
return await res.json();
|
|
1827
|
+
}
|
|
1689
1828
|
const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1690
1829
|
const PORTAL = /^\d{1,12}$/;
|
|
1691
1830
|
const HUTK = /^[a-f0-9]{8,128}$/i;
|
|
@@ -1734,6 +1873,10 @@ function formFieldNames(raw) {
|
|
|
1734
1873
|
return names;
|
|
1735
1874
|
}
|
|
1736
1875
|
const shapeCache = /* @__PURE__ */ new Map();
|
|
1876
|
+
function clearFormShapeCache(formGuid) {
|
|
1877
|
+
if (formGuid) shapeCache.delete(formGuid);
|
|
1878
|
+
else shapeCache.clear();
|
|
1879
|
+
}
|
|
1737
1880
|
async function loadFormShape(apiKey, formGuid) {
|
|
1738
1881
|
const cached = shapeCache.get(formGuid);
|
|
1739
1882
|
if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
|
|
@@ -1829,6 +1972,16 @@ async function submitMarketingForm(input) {
|
|
|
1829
1972
|
}
|
|
1830
1973
|
return result;
|
|
1831
1974
|
}
|
|
1975
|
+
async function syncFieldsToHubspotForm(apiKey, formGuid, definition) {
|
|
1976
|
+
const raw = await fetchHubspotForm(apiKey, formGuid);
|
|
1977
|
+
const extra = missingMappedFields(definition, formFieldNames(raw));
|
|
1978
|
+
if (!extra.length) return [];
|
|
1979
|
+
await patchHubspotForm(apiKey, formGuid, {
|
|
1980
|
+
fieldGroups: appendFieldGroups(raw.fieldGroups ?? [], extra)
|
|
1981
|
+
});
|
|
1982
|
+
clearFormShapeCache(formGuid);
|
|
1983
|
+
return extra.map((field) => field.property);
|
|
1984
|
+
}
|
|
1832
1985
|
function publicForm(entry) {
|
|
1833
1986
|
return {
|
|
1834
1987
|
slug: entry.slug,
|
|
@@ -2196,6 +2349,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2196
2349
|
}
|
|
2197
2350
|
}
|
|
2198
2351
|
let usedFormsApi = false;
|
|
2352
|
+
let sentFieldNames = [];
|
|
2199
2353
|
if (email && apiKey) {
|
|
2200
2354
|
const groups = groupByObject(form.definition, resolution.values);
|
|
2201
2355
|
for (const [object, props] of Object.entries(companyGroups)) {
|
|
@@ -2203,21 +2357,25 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2203
2357
|
}
|
|
2204
2358
|
const partition = await partitionBySchema(apiKey, groups);
|
|
2205
2359
|
rejected = partition.rejected;
|
|
2360
|
+
const contactProps = stringProps(partition.accepted.contact ?? {});
|
|
2206
2361
|
const account = await resolveAccount(strapi);
|
|
2362
|
+
const policy = await resolvePolicy(strapi);
|
|
2207
2363
|
const portalId = sanitizePortalId(account.portalId);
|
|
2208
2364
|
const region = account.region || "eu1";
|
|
2209
2365
|
const ownGuid = typeof form.hubspotFormId === "string" ? form.hubspotFormId.trim() : "";
|
|
2210
2366
|
const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
|
|
2211
|
-
|
|
2367
|
+
const tryFormsApi = policy.submissionMode !== "crm" && Boolean(portalId && formGuid);
|
|
2368
|
+
if (tryFormsApi && portalId && formGuid) {
|
|
2212
2369
|
try {
|
|
2213
2370
|
const shape = await loadFormShape(apiKey, formGuid);
|
|
2214
2371
|
const fields = filterToFormFields(
|
|
2215
|
-
toHsFields({ ...
|
|
2372
|
+
toHsFields({ ...contactProps, email }),
|
|
2216
2373
|
shape?.names ?? null
|
|
2217
2374
|
);
|
|
2218
2375
|
if (fields.length && !fields.some((f) => f.name === "email")) {
|
|
2219
2376
|
fields.unshift({ name: "email", value: email });
|
|
2220
2377
|
}
|
|
2378
|
+
sentFieldNames = fields.map((f) => f.name);
|
|
2221
2379
|
const submitted = await submitMarketingForm({
|
|
2222
2380
|
apiKey,
|
|
2223
2381
|
portalId,
|
|
@@ -2254,12 +2412,28 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2254
2412
|
const result = await strapi.plugin("hubspot").service("submit").upsert({
|
|
2255
2413
|
object: "contact",
|
|
2256
2414
|
idProperty: "email",
|
|
2257
|
-
properties: { ...
|
|
2415
|
+
properties: { ...contactProps, email }
|
|
2258
2416
|
});
|
|
2259
2417
|
if (result.ok) {
|
|
2260
2418
|
hubspotSynced = true;
|
|
2261
2419
|
contactId = result.id;
|
|
2262
2420
|
}
|
|
2421
|
+
} else if (policy.writeExtraProperties) {
|
|
2422
|
+
const extra = leftoverContactProps(contactProps, sentFieldNames);
|
|
2423
|
+
if (Object.keys(extra).length) {
|
|
2424
|
+
try {
|
|
2425
|
+
const result = await strapi.plugin("hubspot").service("submit").upsert({
|
|
2426
|
+
object: "contact",
|
|
2427
|
+
idProperty: "email",
|
|
2428
|
+
properties: { ...extra, email }
|
|
2429
|
+
});
|
|
2430
|
+
if (result.ok) contactId = result.id ?? contactId;
|
|
2431
|
+
} catch (err) {
|
|
2432
|
+
strapi.log.warn(
|
|
2433
|
+
`[hubspot] leftover contact properties failed (conversion OK) — ${err.message}`
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2263
2437
|
}
|
|
2264
2438
|
const domain = corporateDomain(email);
|
|
2265
2439
|
const companyProps = partition.accepted.company ?? {};
|
|
@@ -2558,7 +2732,32 @@ function createFormsAdminController(strapi) {
|
|
|
2558
2732
|
documentId: ctx.params.documentId,
|
|
2559
2733
|
locale: ctx.query.locale
|
|
2560
2734
|
});
|
|
2561
|
-
|
|
2735
|
+
const policy = await resolvePolicy(strapi);
|
|
2736
|
+
let addedFields = [];
|
|
2737
|
+
let syncWarning;
|
|
2738
|
+
if (policy.syncFieldsOnPublish) {
|
|
2739
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
2740
|
+
const account = await resolveAccount(strapi);
|
|
2741
|
+
const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
|
|
2742
|
+
const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
|
|
2743
|
+
if (apiKey && formGuid) {
|
|
2744
|
+
try {
|
|
2745
|
+
addedFields = await syncFieldsToHubspotForm(
|
|
2746
|
+
apiKey,
|
|
2747
|
+
formGuid,
|
|
2748
|
+
entry.definition ?? EMPTY_DEFINITION
|
|
2749
|
+
);
|
|
2750
|
+
} catch (err) {
|
|
2751
|
+
syncWarning = err.message;
|
|
2752
|
+
strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
ctx.body = {
|
|
2757
|
+
ok: true,
|
|
2758
|
+
...addedFields.length ? { addedFields } : {},
|
|
2759
|
+
...syncWarning ? { syncWarning } : {}
|
|
2760
|
+
};
|
|
2562
2761
|
},
|
|
2563
2762
|
async unpublish(ctx) {
|
|
2564
2763
|
await documents().unpublish({
|
|
@@ -3022,10 +3221,20 @@ const config = {
|
|
|
3022
3221
|
// submissions go through the marketing Forms API);
|
|
3023
3222
|
// - defaultFormId: portal-wide marketing form GUID when a builder form
|
|
3024
3223
|
// doesn't set its own — swap with the portal at production cutover.
|
|
3224
|
+
// - submissionMode / writeExtraProperties / syncFieldsOnPublish: see
|
|
3225
|
+
// Settings → HubSpot; each install picks its HubSpot will.
|
|
3025
3226
|
forms: {
|
|
3026
3227
|
companyFromDomain: true,
|
|
3027
3228
|
timelineNote: true,
|
|
3028
3229
|
defaultFormId: "",
|
|
3230
|
+
// How submissions reach HubSpot. `auto` = Forms API when a GUID is set,
|
|
3231
|
+
// else CRM upsert. `crm` skips conversions. `forms` prefers the Forms API.
|
|
3232
|
+
submissionMode: "auto",
|
|
3233
|
+
// After a conversion, CRM-write mapped contact props the HubSpot form dropped.
|
|
3234
|
+
writeExtraProperties: true,
|
|
3235
|
+
// On publish, PATCH missing mapped contact fields onto the HubSpot form.
|
|
3236
|
+
// Off by default: HubSpot-first installs must not have Strapi mutate forms.
|
|
3237
|
+
syncFieldsOnPublish: false,
|
|
3029
3238
|
// Per-IP brakes on the public routes; 0 disables one.
|
|
3030
3239
|
rateLimit: { submitPerMinute: 6, searchPerMinute: 30 }
|
|
3031
3240
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy helpers for HubSpot form field sync and leftover CRM writes.
|
|
3
|
+
*
|
|
4
|
+
* Two independent user wills, neither assumed:
|
|
5
|
+
* - Strapi-first: mapped contact fields missing on the HubSpot form can be
|
|
6
|
+
* PATCHed onto it at publish time (opt-in).
|
|
7
|
+
* - HubSpot-first: extras must not 400 the Forms API submit, but they can
|
|
8
|
+
* still land on the contact via a CRM upsert of the leftovers.
|
|
9
|
+
*
|
|
10
|
+
* CRM properties are never created here — the picker only maps to ones that
|
|
11
|
+
* already exist on the portal.
|
|
12
|
+
*/
|
|
13
|
+
import type { FormDefinition } from "./conditions";
|
|
14
|
+
export interface MappedFormField {
|
|
15
|
+
name: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
type: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
object?: string;
|
|
20
|
+
property: string;
|
|
21
|
+
options?: {
|
|
22
|
+
value?: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
export declare function leftoverContactProps(contact: Record<string, string>, sentFieldNames: Iterable<string>): Record<string, string>;
|
|
27
|
+
export declare function mappedContactFields(definition: FormDefinition): MappedFormField[];
|
|
28
|
+
export declare function missingMappedFields(definition: FormDefinition, existingNames: Iterable<string>): MappedFormField[];
|
|
29
|
+
export declare function hsFieldType(type: string, options?: unknown[]): string;
|
|
30
|
+
export declare function toHsFormField(field: MappedFormField): Record<string, unknown>;
|
|
31
|
+
export interface RawFieldGroupLike {
|
|
32
|
+
groupType?: string;
|
|
33
|
+
richTextType?: string;
|
|
34
|
+
richText?: string;
|
|
35
|
+
fields?: unknown[];
|
|
36
|
+
}
|
|
37
|
+
export declare function appendFieldGroups(existing: RawFieldGroupLike[], extra: MappedFormField[]): Record<string, unknown>[];
|
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
* The form-builder server side: shaping a published form for the public API,
|
|
3
3
|
* and turning a submission into a HubSpot marketing-form conversion.
|
|
4
4
|
*
|
|
5
|
-
* The pipeline
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* on that fallback. Company sync is best-effort; the form submission is
|
|
12
|
-
* the lead.
|
|
5
|
+
* The pipeline is a policy the install picks in Settings: native Forms API
|
|
6
|
+
* conversion (with leftover CRM writes for mapped fields the HubSpot form
|
|
7
|
+
* dropped), CRM upsert only, or automatic (Forms API when a GUID is linked).
|
|
8
|
+
* Company find-or-create still runs after a successful submit. Timeline notes
|
|
9
|
+
* are opt-in and only used on the CRM-upsert path. The form submission is
|
|
10
|
+
* the lead — a CRM outage never loses it.
|
|
13
11
|
*/
|
|
14
12
|
import type { Core } from "@strapi/strapi";
|
|
15
13
|
import { type FormDefinition, type Primitive } from "./conditions";
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Portal id, region and form GUIDs are configuration — a test portal today,
|
|
11
11
|
* production tomorrow, no code change.
|
|
12
12
|
*/
|
|
13
|
+
import type { FormDefinition } from "./conditions";
|
|
13
14
|
export declare function isFormGuid(value: string): boolean;
|
|
14
15
|
export declare function sanitizeHutk(value: unknown): string | undefined;
|
|
15
16
|
export declare function sanitizePortalId(value: unknown): string | undefined;
|
|
@@ -58,6 +59,7 @@ export interface FormSubmitInput {
|
|
|
58
59
|
};
|
|
59
60
|
formHasLegalConsent?: boolean;
|
|
60
61
|
}
|
|
62
|
+
export declare function clearFormShapeCache(formGuid?: string): void;
|
|
61
63
|
export declare function loadFormShape(apiKey: string, formGuid: string): Promise<{
|
|
62
64
|
names: Set<string>;
|
|
63
65
|
hasLegalConsent: boolean;
|
|
@@ -73,3 +75,9 @@ export declare function submitMarketingForm(input: FormSubmitInput): Promise<{
|
|
|
73
75
|
status: number;
|
|
74
76
|
error?: string;
|
|
75
77
|
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Opt-in: add mapped contact fields that the HubSpot form doesn't already
|
|
80
|
+
* declare. Never creates CRM properties — only form fields pointing at ones
|
|
81
|
+
* that already exist. Returns the property names that were added.
|
|
82
|
+
*/
|
|
83
|
+
export declare function syncFieldsToHubspotForm(apiKey: string, formGuid: string, definition: FormDefinition): Promise<string[]>;
|
|
@@ -83,4 +83,8 @@ export declare function convertHubspotForm(raw: RawHubspotForm): ConvertedHubspo
|
|
|
83
83
|
export declare function listHubspotForms(apiKey: string): Promise<HubspotFormSummary[]>;
|
|
84
84
|
/** One form, full definition. */
|
|
85
85
|
export declare function fetchHubspotForm(apiKey: string, formId: string): Promise<RawHubspotForm>;
|
|
86
|
+
/** Replace `fieldGroups` (HubSpot PATCH is a full replacement of that array). */
|
|
87
|
+
export declare function patchHubspotForm(apiKey: string, formId: string, body: {
|
|
88
|
+
fieldGroups: unknown[];
|
|
89
|
+
}): Promise<RawHubspotForm>;
|
|
86
90
|
export {};
|
|
@@ -13,6 +13,9 @@ declare const _default: {
|
|
|
13
13
|
companyFromDomain: boolean;
|
|
14
14
|
timelineNote: boolean;
|
|
15
15
|
defaultFormId: string;
|
|
16
|
+
submissionMode: "auto" | "crm" | "forms";
|
|
17
|
+
writeExtraProperties: boolean;
|
|
18
|
+
syncFieldsOnPublish: boolean;
|
|
16
19
|
rateLimit: {
|
|
17
20
|
submitPerMinute: number;
|
|
18
21
|
searchPerMinute: number;
|
|
@@ -364,6 +367,9 @@ declare const _default: {
|
|
|
364
367
|
portalId?: string;
|
|
365
368
|
region?: string;
|
|
366
369
|
defaultFormId?: string;
|
|
370
|
+
submissionMode?: string;
|
|
371
|
+
writeExtraProperties?: boolean;
|
|
372
|
+
syncFieldsOnPublish?: boolean;
|
|
367
373
|
};
|
|
368
374
|
};
|
|
369
375
|
body: unknown;
|
|
@@ -15,11 +15,23 @@ export declare const ENV_PORTAL = "HUBSPOT_PORTAL_ID";
|
|
|
15
15
|
export declare const ENV_REGION = "HUBSPOT_REGION";
|
|
16
16
|
export declare const ENV_DEFAULT_FORM = "HUBSPOT_DEFAULT_FORM_ID";
|
|
17
17
|
export type SettingSource = "settings" | "config" | "env" | null;
|
|
18
|
+
export type SubmissionMode = "auto" | "crm" | "forms";
|
|
18
19
|
export interface StoredSettings {
|
|
19
20
|
apiKey?: string;
|
|
20
21
|
portalId?: string;
|
|
21
22
|
region?: string;
|
|
22
23
|
defaultFormId?: string;
|
|
24
|
+
submissionMode?: SubmissionMode;
|
|
25
|
+
writeExtraProperties?: boolean;
|
|
26
|
+
syncFieldsOnPublish?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface HubspotPolicy {
|
|
29
|
+
/** How submissions reach HubSpot. `auto` uses Forms API when a GUID is set. */
|
|
30
|
+
submissionMode: SubmissionMode;
|
|
31
|
+
/** After a Forms API conversion, CRM-write mapped contact props the form dropped. */
|
|
32
|
+
writeExtraProperties: boolean;
|
|
33
|
+
/** On publish, PATCH missing mapped contact fields onto the HubSpot form. */
|
|
34
|
+
syncFieldsOnPublish: boolean;
|
|
23
35
|
}
|
|
24
36
|
export interface PublicSettings {
|
|
25
37
|
configured: boolean;
|
|
@@ -33,6 +45,9 @@ export interface PublicSettings {
|
|
|
33
45
|
portalSource: SettingSource;
|
|
34
46
|
regionSource: SettingSource;
|
|
35
47
|
formSource: SettingSource;
|
|
48
|
+
submissionMode: SubmissionMode;
|
|
49
|
+
writeExtraProperties: boolean;
|
|
50
|
+
syncFieldsOnPublish: boolean;
|
|
36
51
|
}
|
|
37
52
|
export interface HubspotAccount {
|
|
38
53
|
portalId: string;
|
|
@@ -41,6 +56,15 @@ export interface HubspotAccount {
|
|
|
41
56
|
}
|
|
42
57
|
export declare function getStoredSettings(strapi: Core.Strapi): Promise<StoredSettings>;
|
|
43
58
|
export declare function setStoredSettings(strapi: Core.Strapi, value: StoredSettings | null): Promise<void>;
|
|
59
|
+
export declare function asSubmissionMode(value: unknown): SubmissionMode | undefined;
|
|
60
|
+
interface FormsCfg {
|
|
61
|
+
defaultFormId?: string;
|
|
62
|
+
submissionMode?: unknown;
|
|
63
|
+
writeExtraProperties?: unknown;
|
|
64
|
+
syncFieldsOnPublish?: unknown;
|
|
65
|
+
}
|
|
66
|
+
export declare function policyFrom(stored: StoredSettings, forms: FormsCfg): HubspotPolicy;
|
|
67
|
+
export declare function resolvePolicy(strapi: Core.Strapi): Promise<HubspotPolicy>;
|
|
44
68
|
export declare function resolveApiKey(strapi: Core.Strapi): Promise<{
|
|
45
69
|
apiKey: string;
|
|
46
70
|
source: SettingSource;
|
|
@@ -48,12 +72,17 @@ export declare function resolveApiKey(strapi: Core.Strapi): Promise<{
|
|
|
48
72
|
/** Portal / region / default marketing form — settings UI overrides env. */
|
|
49
73
|
export declare function resolveAccount(strapi: Core.Strapi): Promise<HubspotAccount>;
|
|
50
74
|
export declare function publicSettings(strapi: Core.Strapi): Promise<PublicSettings>;
|
|
51
|
-
|
|
52
|
-
export declare function patchStoredSettings(strapi: Core.Strapi, body: {
|
|
75
|
+
export interface SettingsPatch {
|
|
53
76
|
apiKey?: string;
|
|
54
77
|
portalId?: unknown;
|
|
55
78
|
region?: unknown;
|
|
56
79
|
defaultFormId?: unknown;
|
|
57
|
-
|
|
80
|
+
submissionMode?: unknown;
|
|
81
|
+
writeExtraProperties?: unknown;
|
|
82
|
+
syncFieldsOnPublish?: unknown;
|
|
83
|
+
}
|
|
84
|
+
/** Merge a PATCH-like body into the store without dropping the saved key. */
|
|
85
|
+
export declare function patchStoredSettings(strapi: Core.Strapi, body: SettingsPatch): Promise<void>;
|
|
58
86
|
/** Drop only the stored token; portal / form settings stay. */
|
|
59
87
|
export declare function clearStoredApiKey(strapi: Core.Strapi): Promise<void>;
|
|
88
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-hubspot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "The form builder for HubSpot in Strapi — multi-step forms with conditional fields mapped to real CRM properties, a public submit pipeline with retries, and save-time mapping validation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|