strapi-plugin-hubspot 0.3.0 → 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.
Files changed (44) hide show
  1. package/README.md +121 -2
  2. package/dist/_chunks/HubspotObjectInput-DAq_W1rA.js +91 -0
  3. package/dist/_chunks/HubspotObjectInput-G-A_pL2j.mjs +73 -0
  4. package/dist/_chunks/HubspotPropertyInput-B-Jg0x68.js +186 -0
  5. package/dist/_chunks/HubspotPropertyInput-Cv77QiS6.mjs +168 -0
  6. package/dist/_chunks/Settings-58edRL5D.mjs +1293 -0
  7. package/dist/_chunks/Settings-CryiUgSe.js +1311 -0
  8. package/dist/_chunks/en-CnQbjHs-.js +81 -0
  9. package/dist/_chunks/en-DHQZuRsj.mjs +81 -0
  10. package/dist/_chunks/fr-Bv8zsNX5.js +81 -0
  11. package/dist/_chunks/fr-Dxgil7RP.mjs +81 -0
  12. package/dist/_chunks/index-BudHTwSw.mjs +137 -0
  13. package/dist/_chunks/index-Tlw1p93d.js +136 -0
  14. package/dist/_chunks/objectLabels-CJTKZ4vT.mjs +425 -0
  15. package/dist/_chunks/objectLabels-GRmYGCIn.js +442 -0
  16. package/dist/admin/index.js +1 -1
  17. package/dist/admin/index.mjs +1 -1
  18. package/dist/admin/src/components/AuditSection.d.ts +5 -0
  19. package/dist/admin/src/components/FailuresSection.d.ts +11 -0
  20. package/dist/admin/src/components/HubspotObjectInput.d.ts +28 -0
  21. package/dist/admin/src/components/HubspotPropertyInput.d.ts +1 -0
  22. package/dist/admin/src/getTranslation.d.ts +3 -0
  23. package/dist/admin/src/index.d.ts +6 -0
  24. package/dist/admin/src/objectLabels.d.ts +2 -1
  25. package/dist/server/index.js +417 -69
  26. package/dist/server/index.mjs +417 -69
  27. package/dist/server/src/__tests__/audit.test.d.ts +1 -0
  28. package/dist/server/src/__tests__/checkMapping.test.d.ts +1 -0
  29. package/dist/server/src/__tests__/loadSchema.test.d.ts +1 -0
  30. package/dist/server/src/__tests__/submit.test.d.ts +1 -0
  31. package/dist/server/src/__tests__/validation.test.d.ts +1 -0
  32. package/dist/server/src/audit.d.ts +60 -0
  33. package/dist/server/src/content-types.d.ts +56 -0
  34. package/dist/server/src/index.d.ts +104 -28
  35. package/dist/server/src/properties.d.ts +7 -2
  36. package/dist/server/src/submit.d.ts +35 -0
  37. package/dist/server/src/validation.d.ts +37 -0
  38. package/package.json +5 -2
  39. package/dist/_chunks/HubspotPropertyInput-C3KbDHYP.mjs +0 -106
  40. package/dist/_chunks/HubspotPropertyInput-C9PpYYgj.js +0 -124
  41. package/dist/_chunks/Settings-DGJ_mmlw.mjs +0 -140
  42. package/dist/_chunks/Settings-Lj49S5fO.js +0 -158
  43. package/dist/_chunks/index-Bq1tS5h6.js +0 -71
  44. package/dist/_chunks/index-lx7um1Yx.mjs +0 -72
@@ -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 hsGet(
30
- apiKey,
31
- `/crm/v3/properties/${object.path}`
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) {
@@ -111,7 +122,8 @@ function checkMapping(properties, mapping) {
111
122
  }
112
123
  if (trimmed !== property) return { code: "whitespace", property, object };
113
124
  if (match.type === "enumeration" && match.options.length && mapping.values?.length) {
114
- const stray = mapping.values.filter((v) => v && !match.options.includes(v));
125
+ const allowed = match.options.map((o) => o.value);
126
+ const stray = mapping.values.filter((v) => v && !allowed.includes(v));
115
127
  if (stray.length) {
116
128
  return { code: "bad-option", property: trimmed, object, values: stray };
117
129
  }
@@ -172,6 +184,337 @@ async function publicSettings(strapi) {
172
184
  hint: apiKey ? `…${apiKey.slice(-4)}` : ""
173
185
  };
174
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";
175
518
  const config = {
176
519
  default: {
177
520
  apiKey: "",
@@ -208,6 +551,37 @@ const controllers = {
208
551
  }
209
552
  }
210
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.");
573
+ }
574
+ }
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
+ }),
211
585
  settings: ({ strapi }) => ({
212
586
  async get(ctx) {
213
587
  ctx.body = await publicSettings(strapi);
@@ -223,91 +597,65 @@ const controllers = {
223
597
  }
224
598
  })
225
599
  };
226
- const adminRoute = (method, path, handler) => ({
600
+ const adminRoute = (method, path, handler, actions) => ({
227
601
  method,
228
602
  path,
229
603
  handler,
230
- config: { policies: ["admin::isAuthenticatedAdmin"] }
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
+ }
231
612
  });
232
613
  const routes = {
233
614
  admin: {
234
615
  type: "admin",
235
616
  routes: [
236
617
  adminRoute("GET", "/properties", "properties.list"),
237
- adminRoute("GET", "/settings", "settings.get"),
238
- adminRoute("PUT", "/settings", "settings.update"),
239
- adminRoute("DELETE", "/settings", "settings.reset")
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])
240
624
  ]
241
625
  }
242
626
  };
243
- function collectMappings(node, target, found = []) {
244
- if (Array.isArray(node)) {
245
- for (const item of node) collectMappings(item, target, found);
246
- return found;
247
- }
248
- if (!node || typeof node !== "object") return found;
249
- const obj = node;
250
- const property = obj[target.propertyField];
251
- if (typeof property === "string" && property.trim()) {
252
- const object = obj[target.objectField];
253
- const rawOptions = obj[target.optionsField || "options"];
254
- const values = Array.isArray(rawOptions) ? rawOptions.map((o) => {
255
- const opt = o ?? {};
256
- const v = typeof opt.value === "string" && opt.value.trim() ? opt.value : opt.label;
257
- return typeof v === "string" ? v.trim() : "";
258
- }).filter(Boolean) : void 0;
259
- found.push({
260
- object: typeof object === "string" && object ? object : "contact",
261
- property,
262
- values
263
- });
264
- }
265
- for (const value of Object.values(obj)) collectMappings(value, target, found);
266
- return found;
267
- }
268
627
  const index = {
269
628
  config,
629
+ contentTypes,
270
630
  controllers,
271
631
  routes,
632
+ services: {
633
+ submit: ({ strapi }) => createSubmitService(strapi)
634
+ },
272
635
  register({ strapi }) {
273
636
  strapi.customFields.register({
274
637
  name: "property",
275
638
  plugin: "hubspot",
276
639
  type: "string"
277
640
  });
641
+ strapi.customFields.register({
642
+ name: "object",
643
+ plugin: "hubspot",
644
+ type: "string"
645
+ });
278
646
  },
279
- 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
+ ]);
280
656
  const targets = strapi.plugin("hubspot").config("validate", []);
281
- if (!targets.length) return;
282
657
  for (const target of targets) {
283
- strapi.documents.use(async (context, next) => {
284
- const isWrite = ["create", "update"].includes(context.action);
285
- if (!isWrite || context.uid !== target.uid) return next();
286
- const mappings = collectMappings(context.params?.data, target);
287
- if (!mappings.length) return next();
288
- const { apiKey } = await resolveApiKey(strapi);
289
- if (!apiKey) return next();
290
- let schema;
291
- try {
292
- schema = await loadSchema(
293
- strapi,
294
- apiKey,
295
- resolveObjects(strapi.plugin("hubspot").config("objects", []))
296
- );
297
- } catch {
298
- strapi.log.warn("[hubspot] schema unavailable — validation skipped");
299
- return next();
300
- }
301
- const problems = mappings.map((m) => checkMapping(schema.properties, m)).filter((p) => Boolean(p));
302
- if (problems.length) {
303
- const sentences = [...new Set(problems.map(describeProblem))];
304
- throw new utils.errors.ValidationError(
305
- `Invalid HubSpot mapping — ${sentences.join("; ")}`,
306
- { problems }
307
- );
308
- }
309
- return next();
310
- });
658
+ strapi.documents.use(makeValidationMiddleware(strapi, target));
311
659
  }
312
660
  }
313
661
  };