strapi-plugin-hubspot 0.2.1 → 0.4.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 +145 -5
- package/dist/_chunks/HubspotObjectInput-DAq_W1rA.js +91 -0
- package/dist/_chunks/HubspotObjectInput-G-A_pL2j.mjs +73 -0
- package/dist/_chunks/HubspotPropertyInput-B-Jg0x68.js +186 -0
- package/dist/_chunks/HubspotPropertyInput-Cv77QiS6.mjs +168 -0
- package/dist/_chunks/Settings-58edRL5D.mjs +1293 -0
- package/dist/_chunks/Settings-CryiUgSe.js +1311 -0
- package/dist/_chunks/en-CnQbjHs-.js +81 -0
- package/dist/_chunks/en-DHQZuRsj.mjs +81 -0
- package/dist/_chunks/fr-Bv8zsNX5.js +81 -0
- package/dist/_chunks/fr-Dxgil7RP.mjs +81 -0
- package/dist/_chunks/index-BudHTwSw.mjs +137 -0
- package/dist/_chunks/index-Tlw1p93d.js +136 -0
- package/dist/_chunks/objectLabels-CJTKZ4vT.mjs +425 -0
- package/dist/_chunks/objectLabels-GRmYGCIn.js +442 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/components/AuditSection.d.ts +5 -0
- package/dist/admin/src/components/FailuresSection.d.ts +11 -0
- package/dist/admin/src/components/HubspotObjectInput.d.ts +28 -0
- package/dist/admin/src/components/HubspotPropertyInput.d.ts +1 -0
- package/dist/admin/src/getTranslation.d.ts +3 -0
- package/dist/admin/src/index.d.ts +6 -0
- package/dist/admin/src/objectLabels.d.ts +2 -1
- package/dist/server/index.js +445 -69
- package/dist/server/index.mjs +445 -69
- package/dist/server/src/__tests__/audit.test.d.ts +1 -0
- package/dist/server/src/__tests__/checkMapping.test.d.ts +1 -0
- package/dist/server/src/__tests__/loadSchema.test.d.ts +1 -0
- package/dist/server/src/__tests__/submit.test.d.ts +1 -0
- package/dist/server/src/__tests__/validation.test.d.ts +1 -0
- package/dist/server/src/audit.d.ts +60 -0
- package/dist/server/src/content-types.d.ts +56 -0
- package/dist/server/src/index.d.ts +104 -22
- package/dist/server/src/properties.d.ts +41 -5
- package/dist/server/src/submit.d.ts +35 -0
- package/dist/server/src/validation.d.ts +37 -0
- package/package.json +5 -2
- package/dist/_chunks/HubspotPropertyInput-BVFRtt49.js +0 -124
- package/dist/_chunks/HubspotPropertyInput-C7Wcw8ee.mjs +0 -106
- package/dist/_chunks/Settings-B5w7LAsH.mjs +0 -140
- package/dist/_chunks/Settings-TGB0MOHr.js +0 -158
- package/dist/_chunks/index-BpZtcsPM.mjs +0 -42
- package/dist/_chunks/index-DgsAHPui.js +0 -41
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const utils = require("@strapi/utils");
|
|
3
|
-
const HS_BASE = "https://api.hubapi.com";
|
|
3
|
+
const HS_BASE$1 = "https://api.hubapi.com";
|
|
4
4
|
const TTL_MS = 10 * 60 * 1e3;
|
|
5
5
|
const STANDARD_OBJECTS = [
|
|
6
6
|
{ name: "contact", path: "contacts" },
|
|
@@ -14,7 +14,7 @@ const STANDARD_OBJECTS = [
|
|
|
14
14
|
let cache = null;
|
|
15
15
|
let inFlight = null;
|
|
16
16
|
async function hsGet(apiKey, path) {
|
|
17
|
-
const res = await fetch(`${HS_BASE}${path}`, {
|
|
17
|
+
const res = await fetch(`${HS_BASE$1}${path}`, {
|
|
18
18
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
19
19
|
});
|
|
20
20
|
if (!res.ok) {
|
|
@@ -25,18 +25,29 @@ async function hsGet(apiKey, path) {
|
|
|
25
25
|
}
|
|
26
26
|
return await res.json();
|
|
27
27
|
}
|
|
28
|
+
async function fetchGroups(apiKey, path) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await hsGet(
|
|
31
|
+
apiKey,
|
|
32
|
+
`/crm/v3/properties/${path}/groups`
|
|
33
|
+
);
|
|
34
|
+
return Object.fromEntries((res.results ?? []).map((g) => [g.name, g.label || g.name]));
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
28
39
|
async function fetchObject(apiKey, object) {
|
|
29
|
-
const res = await
|
|
30
|
-
apiKey,
|
|
31
|
-
|
|
32
|
-
);
|
|
40
|
+
const [res, groups] = await Promise.all([
|
|
41
|
+
hsGet(apiKey, `/crm/v3/properties/${object.path}`),
|
|
42
|
+
fetchGroups(apiKey, object.path)
|
|
43
|
+
]);
|
|
33
44
|
return (res.results ?? []).filter((p) => !p.modificationMetadata?.readOnlyValue).map((p) => ({
|
|
34
45
|
name: p.name,
|
|
35
46
|
label: p.label || p.name,
|
|
36
47
|
object: object.name,
|
|
37
48
|
type: p.type,
|
|
38
|
-
options: (p.options ?? []).map((o) => o.value),
|
|
39
|
-
group: p.groupName
|
|
49
|
+
options: (p.options ?? []).map((o) => ({ value: o.value, label: o.label })),
|
|
50
|
+
group: p.groupName ? groups[p.groupName] ?? p.groupName : void 0
|
|
40
51
|
}));
|
|
41
52
|
}
|
|
42
53
|
async function fetchAccount(apiKey) {
|
|
@@ -69,14 +80,14 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
|
69
80
|
for (const entry of settled) {
|
|
70
81
|
if ("error" in entry && entry.error) {
|
|
71
82
|
unavailable.push({ object: entry.object.name, reason: entry.error });
|
|
72
|
-
strapi.log.warn(`[hubspot] ${entry.object.name}
|
|
83
|
+
strapi.log.warn(`[hubspot] ${entry.object.name} unreadable — ${entry.error}`);
|
|
73
84
|
continue;
|
|
74
85
|
}
|
|
75
86
|
available.push(entry.object.name);
|
|
76
87
|
properties.push(...entry.properties ?? []);
|
|
77
88
|
}
|
|
78
89
|
if (!available.length) {
|
|
79
|
-
throw new Error(unavailable[0]?.reason ?? "
|
|
90
|
+
throw new Error(unavailable[0]?.reason ?? "No readable object");
|
|
80
91
|
}
|
|
81
92
|
properties.sort(
|
|
82
93
|
(a, b) => a.object.localeCompare(b.object) || a.label.localeCompare(b.label)
|
|
@@ -100,16 +111,36 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
|
100
111
|
function clearCache() {
|
|
101
112
|
cache = null;
|
|
102
113
|
}
|
|
103
|
-
function
|
|
104
|
-
const
|
|
114
|
+
function checkMapping(properties, mapping) {
|
|
115
|
+
const { object, property } = mapping;
|
|
116
|
+
const trimmed = property.trim();
|
|
105
117
|
if (!trimmed) return null;
|
|
106
118
|
const match = properties.find((p) => p.object === object && p.name === trimmed);
|
|
107
|
-
if (match)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
if (!match) {
|
|
120
|
+
const elsewhere = properties.find((p) => p.name === trimmed);
|
|
121
|
+
return elsewhere ? { code: "wrong-object", property: trimmed, object, actualObject: elsewhere.object } : { code: "unknown", property: trimmed, object };
|
|
122
|
+
}
|
|
123
|
+
if (trimmed !== property) return { code: "whitespace", property, object };
|
|
124
|
+
if (match.type === "enumeration" && match.options.length && mapping.values?.length) {
|
|
125
|
+
const allowed = match.options.map((o) => o.value);
|
|
126
|
+
const stray = mapping.values.filter((v) => v && !allowed.includes(v));
|
|
127
|
+
if (stray.length) {
|
|
128
|
+
return { code: "bad-option", property: trimmed, object, values: stray };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function describeProblem(problem) {
|
|
134
|
+
switch (problem.code) {
|
|
135
|
+
case "unknown":
|
|
136
|
+
return `"${problem.property}" does not exist on ${problem.object} in this HubSpot portal`;
|
|
137
|
+
case "wrong-object":
|
|
138
|
+
return `"${problem.property}" exists on ${problem.actualObject}, not on ${problem.object}`;
|
|
139
|
+
case "whitespace":
|
|
140
|
+
return `"${problem.property}" has surrounding whitespace`;
|
|
141
|
+
case "bad-option":
|
|
142
|
+
return `"${problem.property}" does not accept ${problem.values.map((v) => `"${v}"`).join(", ")}`;
|
|
111
143
|
}
|
|
112
|
-
return `« ${trimmed} » n'existe pas dans ce portail HubSpot`;
|
|
113
144
|
}
|
|
114
145
|
function resolveObjects(configured) {
|
|
115
146
|
if (!Array.isArray(configured) || !configured.length) {
|
|
@@ -153,6 +184,337 @@ async function publicSettings(strapi) {
|
|
|
153
184
|
hint: apiKey ? `…${apiKey.slice(-4)}` : ""
|
|
154
185
|
};
|
|
155
186
|
}
|
|
187
|
+
function collectMappings(node, target, found = []) {
|
|
188
|
+
if (Array.isArray(node)) {
|
|
189
|
+
for (const item of node) collectMappings(item, target, found);
|
|
190
|
+
return found;
|
|
191
|
+
}
|
|
192
|
+
if (!node || typeof node !== "object") return found;
|
|
193
|
+
const obj = node;
|
|
194
|
+
const property = obj[target.propertyField];
|
|
195
|
+
if (typeof property === "string" && property.trim()) {
|
|
196
|
+
const object = obj[target.objectField];
|
|
197
|
+
const rawOptions = obj[target.optionsField || "options"];
|
|
198
|
+
const values = Array.isArray(rawOptions) ? rawOptions.map((o) => {
|
|
199
|
+
const opt = o ?? {};
|
|
200
|
+
const v = typeof opt.value === "string" && opt.value.trim() ? opt.value : opt.label;
|
|
201
|
+
return typeof v === "string" ? v.trim() : "";
|
|
202
|
+
}).filter(Boolean) : void 0;
|
|
203
|
+
found.push({
|
|
204
|
+
object: typeof object === "string" && object ? object : "contact",
|
|
205
|
+
property,
|
|
206
|
+
values
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
for (const value of Object.values(obj)) collectMappings(value, target, found);
|
|
210
|
+
return found;
|
|
211
|
+
}
|
|
212
|
+
function makeValidationMiddleware(strapi, target) {
|
|
213
|
+
return async (context, next) => {
|
|
214
|
+
const isWrite = ["create", "update"].includes(context.action);
|
|
215
|
+
if (!isWrite || context.uid !== target.uid) return next();
|
|
216
|
+
const mappings = collectMappings(
|
|
217
|
+
context.params?.data,
|
|
218
|
+
target
|
|
219
|
+
);
|
|
220
|
+
if (!mappings.length) return next();
|
|
221
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
222
|
+
if (!apiKey) return next();
|
|
223
|
+
let schema;
|
|
224
|
+
try {
|
|
225
|
+
schema = await loadSchema(
|
|
226
|
+
strapi,
|
|
227
|
+
apiKey,
|
|
228
|
+
resolveObjects(strapi.plugin("hubspot").config("objects", []))
|
|
229
|
+
);
|
|
230
|
+
} catch {
|
|
231
|
+
strapi.log.warn("[hubspot] schema unavailable — validation skipped");
|
|
232
|
+
return next();
|
|
233
|
+
}
|
|
234
|
+
const problems = mappings.map((m) => checkMapping(schema.properties, m)).filter((p) => Boolean(p));
|
|
235
|
+
const blocking = target.strict === false ? problems.filter((p) => p.code !== "unknown") : problems;
|
|
236
|
+
const soft = problems.filter((p) => !blocking.includes(p));
|
|
237
|
+
if (soft.length) {
|
|
238
|
+
const sentences = [...new Set(soft.map(describeProblem))];
|
|
239
|
+
strapi.log.warn(
|
|
240
|
+
`[hubspot] ${target.uid}: ${sentences.join("; ")} — allowed (strict: false)`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (blocking.length) {
|
|
244
|
+
const sentences = [...new Set(blocking.map(describeProblem))];
|
|
245
|
+
throw new utils.errors.ValidationError(
|
|
246
|
+
`Invalid HubSpot mapping — ${sentences.join("; ")}`,
|
|
247
|
+
{ problems: blocking }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return next();
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function buildPopulate(components, schema, depth = 0) {
|
|
254
|
+
if (depth > 8) return {};
|
|
255
|
+
const populate = {};
|
|
256
|
+
for (const [key, attr] of Object.entries(schema.attributes ?? {})) {
|
|
257
|
+
if (attr.type === "component" && attr.component) {
|
|
258
|
+
const child = components[attr.component];
|
|
259
|
+
const nested = child ? buildPopulate(components, child, depth + 1) : {};
|
|
260
|
+
populate[key] = { populate: Object.keys(nested).length ? nested : "*" };
|
|
261
|
+
} else if (attr.type === "dynamiczone" && attr.components?.length) {
|
|
262
|
+
const on = {};
|
|
263
|
+
for (const uid of attr.components) {
|
|
264
|
+
const child = components[uid];
|
|
265
|
+
const nested = child ? buildPopulate(components, child, depth + 1) : {};
|
|
266
|
+
on[uid] = { populate: Object.keys(nested).length ? nested : "*" };
|
|
267
|
+
}
|
|
268
|
+
populate[key] = { on };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return populate;
|
|
272
|
+
}
|
|
273
|
+
function entryLabel(entry) {
|
|
274
|
+
for (const key of ["title", "name", "label", "heading", "slug"]) {
|
|
275
|
+
const value = entry[key];
|
|
276
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
277
|
+
}
|
|
278
|
+
return String(entry.documentId ?? entry.id ?? "?");
|
|
279
|
+
}
|
|
280
|
+
async function fetchEntries(strapi, uid, schema, populate) {
|
|
281
|
+
const params = {};
|
|
282
|
+
if (Object.keys(populate).length) params.populate = populate;
|
|
283
|
+
if (schema.options?.draftAndPublish) params.status = "draft";
|
|
284
|
+
let locales = [void 0];
|
|
285
|
+
if (schema.pluginOptions?.i18n?.localized) {
|
|
286
|
+
try {
|
|
287
|
+
const known = await strapi.plugin("i18n").service("locales").find();
|
|
288
|
+
if (known.length) locales = known.map((l) => l.code);
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const locale of locales) {
|
|
294
|
+
const docs = await strapi.documents(uid).findMany({
|
|
295
|
+
...params,
|
|
296
|
+
...locale ? { locale } : {}
|
|
297
|
+
});
|
|
298
|
+
for (const doc of docs ?? []) out.push(locale ? { ...doc, __auditLocale: locale } : doc);
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
async function runAudit(strapi, targets, schema) {
|
|
303
|
+
const reports = [];
|
|
304
|
+
for (const target of targets) {
|
|
305
|
+
const contentType = strapi.contentType(target.uid);
|
|
306
|
+
if (!contentType) {
|
|
307
|
+
reports.push({
|
|
308
|
+
uid: target.uid,
|
|
309
|
+
entries: 0,
|
|
310
|
+
mappings: 0,
|
|
311
|
+
invalid: [],
|
|
312
|
+
error: `unknown content type "${target.uid}"`
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const populate = buildPopulate(
|
|
317
|
+
strapi.components,
|
|
318
|
+
contentType
|
|
319
|
+
);
|
|
320
|
+
let entries;
|
|
321
|
+
try {
|
|
322
|
+
entries = await fetchEntries(strapi, target.uid, contentType, populate);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
reports.push({
|
|
325
|
+
uid: target.uid,
|
|
326
|
+
entries: 0,
|
|
327
|
+
mappings: 0,
|
|
328
|
+
invalid: [],
|
|
329
|
+
error: err.message
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const report = {
|
|
334
|
+
uid: target.uid,
|
|
335
|
+
entries: entries.length,
|
|
336
|
+
mappings: 0,
|
|
337
|
+
invalid: []
|
|
338
|
+
};
|
|
339
|
+
for (const entry of entries) {
|
|
340
|
+
const mappings = collectMappings(entry, target);
|
|
341
|
+
report.mappings += mappings.length;
|
|
342
|
+
const problems = mappings.map((m) => checkMapping(schema.properties, m)).filter((p) => Boolean(p));
|
|
343
|
+
if (problems.length) {
|
|
344
|
+
report.invalid.push({
|
|
345
|
+
documentId: String(entry.documentId ?? entry.id ?? "?"),
|
|
346
|
+
locale: entry.__auditLocale ?? void 0,
|
|
347
|
+
label: entryLabel(entry),
|
|
348
|
+
problems
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
reports.push(report);
|
|
353
|
+
}
|
|
354
|
+
return reports;
|
|
355
|
+
}
|
|
356
|
+
const contentTypes = {
|
|
357
|
+
failure: {
|
|
358
|
+
schema: {
|
|
359
|
+
kind: "collectionType",
|
|
360
|
+
collectionName: "hubspot_failures",
|
|
361
|
+
info: {
|
|
362
|
+
singularName: "failure",
|
|
363
|
+
pluralName: "failures",
|
|
364
|
+
displayName: "HubSpot failed submissions",
|
|
365
|
+
description: "Submissions HubSpot couldn't take, waiting to be replayed"
|
|
366
|
+
},
|
|
367
|
+
options: { draftAndPublish: false },
|
|
368
|
+
pluginOptions: {
|
|
369
|
+
"content-manager": { visible: true },
|
|
370
|
+
"content-type-builder": { visible: false }
|
|
371
|
+
},
|
|
372
|
+
attributes: {
|
|
373
|
+
object: { type: "string", required: true },
|
|
374
|
+
idProperty: { type: "string", required: true },
|
|
375
|
+
properties: { type: "json", required: true },
|
|
376
|
+
error: { type: "text" },
|
|
377
|
+
attempts: { type: "integer", default: 1 }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
const HS_BASE = "https://api.hubapi.com";
|
|
383
|
+
const FAILURE_UID = "plugin::hubspot.failure";
|
|
384
|
+
const RETRY_DELAYS_MS = [500, 2e3];
|
|
385
|
+
const sleepFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
386
|
+
function coerceProperties(properties) {
|
|
387
|
+
const out = {};
|
|
388
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
389
|
+
if (value === null || value === void 0 || value === "") continue;
|
|
390
|
+
out[key] = String(value);
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
class HsError extends Error {
|
|
395
|
+
constructor(message, status) {
|
|
396
|
+
super(message);
|
|
397
|
+
this.status = status;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const isTransient = (err) => !(err instanceof HsError) || err.status === 429 || err.status >= 500;
|
|
401
|
+
function createSubmitService(strapi, { sleep = sleepFor } = {}) {
|
|
402
|
+
const objectPath = (object) => resolveObjects(strapi.plugin("hubspot").config("objects", [])).find(
|
|
403
|
+
(o) => o.name === object
|
|
404
|
+
)?.path ?? object;
|
|
405
|
+
async function sendOnce(apiKey, input, properties) {
|
|
406
|
+
const res = await fetch(`${HS_BASE}/crm/v3/objects/${objectPath(input.object)}/batch/upsert`, {
|
|
407
|
+
method: "POST",
|
|
408
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
409
|
+
body: JSON.stringify({
|
|
410
|
+
inputs: [{ idProperty: input.idProperty, id: properties[input.idProperty], properties }]
|
|
411
|
+
})
|
|
412
|
+
});
|
|
413
|
+
const body = await res.json().catch(() => ({}));
|
|
414
|
+
if (!res.ok) throw new HsError(body.message || `HubSpot ${res.status}`, res.status);
|
|
415
|
+
return body.results?.[0]?.id ?? "";
|
|
416
|
+
}
|
|
417
|
+
async function sendWithRetries(apiKey, input, properties) {
|
|
418
|
+
let lastError;
|
|
419
|
+
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
420
|
+
if (attempt > 0) await sleep(RETRY_DELAYS_MS[attempt - 1]);
|
|
421
|
+
try {
|
|
422
|
+
return await sendOnce(apiKey, input, properties);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
lastError = err;
|
|
425
|
+
if (!isTransient(err)) throw err;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
throw lastError;
|
|
429
|
+
}
|
|
430
|
+
async function validateAgainstSchema(apiKey, input, properties) {
|
|
431
|
+
let schema;
|
|
432
|
+
try {
|
|
433
|
+
schema = await loadSchema(
|
|
434
|
+
strapi,
|
|
435
|
+
apiKey,
|
|
436
|
+
resolveObjects(strapi.plugin("hubspot").config("objects", []))
|
|
437
|
+
);
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
const problems = [];
|
|
442
|
+
for (const [name, value] of Object.entries(properties)) {
|
|
443
|
+
const problem = checkMapping(schema.properties, {
|
|
444
|
+
object: input.object,
|
|
445
|
+
property: name,
|
|
446
|
+
values: value.split(";").map((v) => v.trim()).filter(Boolean)
|
|
447
|
+
});
|
|
448
|
+
if (problem) problems.push(problem);
|
|
449
|
+
}
|
|
450
|
+
return problems;
|
|
451
|
+
}
|
|
452
|
+
async function upsert(input, { queueOnFailure = true } = {}) {
|
|
453
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
454
|
+
if (!apiKey) return { ok: false, error: "No HubSpot API key configured" };
|
|
455
|
+
const properties = coerceProperties(input.properties);
|
|
456
|
+
if (!properties[input.idProperty]) {
|
|
457
|
+
return { ok: false, error: `Missing value for idProperty "${input.idProperty}"` };
|
|
458
|
+
}
|
|
459
|
+
const problems = await validateAgainstSchema(apiKey, input, properties);
|
|
460
|
+
if (problems?.length) return { ok: false, problems };
|
|
461
|
+
try {
|
|
462
|
+
const id = await sendWithRetries(apiKey, input, properties);
|
|
463
|
+
return { ok: true, id };
|
|
464
|
+
} catch (err) {
|
|
465
|
+
const message = err.message;
|
|
466
|
+
if (!isTransient(err) || !queueOnFailure) {
|
|
467
|
+
strapi.log.error(`[hubspot] upsert refused — ${message}`);
|
|
468
|
+
return { ok: false, error: message };
|
|
469
|
+
}
|
|
470
|
+
strapi.log.warn(`[hubspot] upsert failed, queued for retry — ${message}`);
|
|
471
|
+
try {
|
|
472
|
+
await strapi.documents(FAILURE_UID).create({
|
|
473
|
+
data: {
|
|
474
|
+
object: input.object,
|
|
475
|
+
idProperty: input.idProperty,
|
|
476
|
+
properties: input.properties,
|
|
477
|
+
error: message,
|
|
478
|
+
attempts: 1
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
return { ok: false, queued: true, error: message };
|
|
482
|
+
} catch (storeErr) {
|
|
483
|
+
strapi.log.error(`[hubspot] could not queue the failure — ${storeErr.message}`);
|
|
484
|
+
return { ok: false, queued: false, error: message };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async function retryFailures({ limit = 50 } = {}) {
|
|
489
|
+
const rows = await strapi.documents(FAILURE_UID).findMany({
|
|
490
|
+
limit,
|
|
491
|
+
sort: "createdAt:asc"
|
|
492
|
+
});
|
|
493
|
+
let succeeded = 0;
|
|
494
|
+
for (const row of rows) {
|
|
495
|
+
const result = await upsert(
|
|
496
|
+
{ object: row.object, idProperty: row.idProperty, properties: row.properties ?? {} },
|
|
497
|
+
{ queueOnFailure: false }
|
|
498
|
+
// Already queued — don't duplicate the row.
|
|
499
|
+
);
|
|
500
|
+
if (result.ok) {
|
|
501
|
+
await strapi.documents(FAILURE_UID).delete({ documentId: row.documentId });
|
|
502
|
+
succeeded += 1;
|
|
503
|
+
} else {
|
|
504
|
+
await strapi.documents(FAILURE_UID).update({
|
|
505
|
+
documentId: row.documentId,
|
|
506
|
+
data: {
|
|
507
|
+
attempts: (row.attempts ?? 1) + 1,
|
|
508
|
+
error: result.error ?? (result.problems ? `invalid mapping: ${JSON.stringify(result.problems)}` : "unknown")
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return { retried: rows.length, succeeded, failed: rows.length - succeeded };
|
|
514
|
+
}
|
|
515
|
+
return { upsert, retryFailures };
|
|
516
|
+
}
|
|
517
|
+
const SETTINGS_ACTION = "plugin::hubspot.settings";
|
|
156
518
|
const config = {
|
|
157
519
|
default: {
|
|
158
520
|
apiKey: "",
|
|
@@ -185,10 +547,41 @@ const controllers = {
|
|
|
185
547
|
ctx.body = { configured: true, ...schema };
|
|
186
548
|
} catch (err) {
|
|
187
549
|
strapi.log.error(`[hubspot] ${err.message}`);
|
|
188
|
-
ctx.throw(502, "
|
|
550
|
+
ctx.throw(502, "Cannot reach HubSpot — check the API key.");
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}),
|
|
554
|
+
audit: ({ strapi }) => ({
|
|
555
|
+
async run(ctx) {
|
|
556
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
557
|
+
if (!apiKey) {
|
|
558
|
+
ctx.body = { configured: false, targets: [] };
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const targets = strapi.plugin("hubspot").config("validate", []);
|
|
562
|
+
try {
|
|
563
|
+
const schema = await loadSchema(
|
|
564
|
+
strapi,
|
|
565
|
+
apiKey,
|
|
566
|
+
resolveObjects(strapi.plugin("hubspot").config("objects", [])),
|
|
567
|
+
{ force: true }
|
|
568
|
+
);
|
|
569
|
+
ctx.body = { configured: true, targets: await runAudit(strapi, targets, schema) };
|
|
570
|
+
} catch (err) {
|
|
571
|
+
strapi.log.error(`[hubspot] audit failed — ${err.message}`);
|
|
572
|
+
ctx.throw(502, "Cannot reach HubSpot — check the API key.");
|
|
189
573
|
}
|
|
190
574
|
}
|
|
191
575
|
}),
|
|
576
|
+
failures: ({ strapi }) => ({
|
|
577
|
+
async list(ctx) {
|
|
578
|
+
const total = await strapi.documents(FAILURE_UID).count({});
|
|
579
|
+
ctx.body = { total };
|
|
580
|
+
},
|
|
581
|
+
async retry(ctx) {
|
|
582
|
+
ctx.body = await strapi.plugin("hubspot").service("submit").retryFailures();
|
|
583
|
+
}
|
|
584
|
+
}),
|
|
192
585
|
settings: ({ strapi }) => ({
|
|
193
586
|
async get(ctx) {
|
|
194
587
|
ctx.body = await publicSettings(strapi);
|
|
@@ -204,82 +597,65 @@ const controllers = {
|
|
|
204
597
|
}
|
|
205
598
|
})
|
|
206
599
|
};
|
|
207
|
-
const adminRoute = (method, path, handler) => ({
|
|
600
|
+
const adminRoute = (method, path, handler, actions) => ({
|
|
208
601
|
method,
|
|
209
602
|
path,
|
|
210
603
|
handler,
|
|
211
|
-
config: {
|
|
604
|
+
config: {
|
|
605
|
+
policies: [
|
|
606
|
+
"admin::isAuthenticatedAdmin",
|
|
607
|
+
// The token is a portal-wide secret; reading properties is not. Only the
|
|
608
|
+
// settings routes carry the extra permission.
|
|
609
|
+
...actions ? [{ name: "admin::hasPermissions", config: { actions } }] : []
|
|
610
|
+
]
|
|
611
|
+
}
|
|
212
612
|
});
|
|
213
613
|
const routes = {
|
|
214
614
|
admin: {
|
|
215
615
|
type: "admin",
|
|
216
616
|
routes: [
|
|
217
617
|
adminRoute("GET", "/properties", "properties.list"),
|
|
218
|
-
adminRoute("GET", "/
|
|
219
|
-
adminRoute("
|
|
220
|
-
adminRoute("
|
|
618
|
+
adminRoute("GET", "/audit", "audit.run", [SETTINGS_ACTION]),
|
|
619
|
+
adminRoute("GET", "/failures", "failures.list", [SETTINGS_ACTION]),
|
|
620
|
+
adminRoute("POST", "/failures/retry", "failures.retry", [SETTINGS_ACTION]),
|
|
621
|
+
adminRoute("GET", "/settings", "settings.get", [SETTINGS_ACTION]),
|
|
622
|
+
adminRoute("PUT", "/settings", "settings.update", [SETTINGS_ACTION]),
|
|
623
|
+
adminRoute("DELETE", "/settings", "settings.reset", [SETTINGS_ACTION])
|
|
221
624
|
]
|
|
222
625
|
}
|
|
223
626
|
};
|
|
224
|
-
function collectMappings(node, target, found = []) {
|
|
225
|
-
if (Array.isArray(node)) {
|
|
226
|
-
for (const item of node) collectMappings(item, target, found);
|
|
227
|
-
return found;
|
|
228
|
-
}
|
|
229
|
-
if (!node || typeof node !== "object") return found;
|
|
230
|
-
const obj = node;
|
|
231
|
-
const property = obj[target.propertyField];
|
|
232
|
-
if (typeof property === "string" && property.trim()) {
|
|
233
|
-
const object = obj[target.objectField];
|
|
234
|
-
found.push({
|
|
235
|
-
object: typeof object === "string" && object ? object : "contact",
|
|
236
|
-
property
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
for (const value of Object.values(obj)) collectMappings(value, target, found);
|
|
240
|
-
return found;
|
|
241
|
-
}
|
|
242
627
|
const index = {
|
|
243
628
|
config,
|
|
629
|
+
contentTypes,
|
|
244
630
|
controllers,
|
|
245
631
|
routes,
|
|
632
|
+
services: {
|
|
633
|
+
submit: ({ strapi }) => createSubmitService(strapi)
|
|
634
|
+
},
|
|
246
635
|
register({ strapi }) {
|
|
247
636
|
strapi.customFields.register({
|
|
248
637
|
name: "property",
|
|
249
638
|
plugin: "hubspot",
|
|
250
639
|
type: "string"
|
|
251
640
|
});
|
|
641
|
+
strapi.customFields.register({
|
|
642
|
+
name: "object",
|
|
643
|
+
plugin: "hubspot",
|
|
644
|
+
type: "string"
|
|
645
|
+
});
|
|
252
646
|
},
|
|
253
|
-
bootstrap({ strapi }) {
|
|
647
|
+
async bootstrap({ strapi }) {
|
|
648
|
+
await strapi.service("admin::permission").actionProvider.registerMany([
|
|
649
|
+
{
|
|
650
|
+
section: "plugins",
|
|
651
|
+
displayName: "Access the HubSpot settings",
|
|
652
|
+
uid: "settings",
|
|
653
|
+
pluginName: "hubspot"
|
|
654
|
+
}
|
|
655
|
+
]);
|
|
254
656
|
const targets = strapi.plugin("hubspot").config("validate", []);
|
|
255
|
-
if (!targets.length) return;
|
|
256
657
|
for (const target of targets) {
|
|
257
|
-
strapi.documents.use(
|
|
258
|
-
const isWrite = ["create", "update"].includes(context.action);
|
|
259
|
-
if (!isWrite || context.uid !== target.uid) return next();
|
|
260
|
-
const mappings = collectMappings(context.params?.data, target);
|
|
261
|
-
if (!mappings.length) return next();
|
|
262
|
-
const { apiKey } = await resolveApiKey(strapi);
|
|
263
|
-
if (!apiKey) return next();
|
|
264
|
-
let schema;
|
|
265
|
-
try {
|
|
266
|
-
schema = await loadSchema(
|
|
267
|
-
strapi,
|
|
268
|
-
apiKey,
|
|
269
|
-
resolveObjects(strapi.plugin("hubspot").config("objects", []))
|
|
270
|
-
);
|
|
271
|
-
} catch {
|
|
272
|
-
strapi.log.warn("[hubspot] schéma indisponible — validation ignorée");
|
|
273
|
-
return next();
|
|
274
|
-
}
|
|
275
|
-
const problems = mappings.map((m) => checkProperty(schema.properties, m.object, m.property)).filter((reason) => Boolean(reason));
|
|
276
|
-
if (problems.length) {
|
|
277
|
-
throw new utils.errors.ValidationError(
|
|
278
|
-
`Mapping HubSpot invalide — ${[...new Set(problems)].join(" ; ")}`
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
return next();
|
|
282
|
-
});
|
|
658
|
+
strapi.documents.use(makeValidationMiddleware(strapi, target));
|
|
283
659
|
}
|
|
284
660
|
}
|
|
285
661
|
};
|