strapi-plugin-hubspot 0.2.0 → 0.3.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 +34 -5
- package/dist/_chunks/{HubspotPropertyInput-D0gijgli.mjs → HubspotPropertyInput-C3KbDHYP.mjs} +2 -2
- package/dist/_chunks/{HubspotPropertyInput-BcdqfAfv.js → HubspotPropertyInput-C9PpYYgj.js} +2 -2
- package/dist/_chunks/{Settings-DuYUpD8L.mjs → Settings-DGJ_mmlw.mjs} +1 -1
- package/dist/_chunks/{Settings-DedA0z6w.js → Settings-Lj49S5fO.js} +1 -1
- package/dist/_chunks/index-Bq1tS5h6.js +71 -0
- package/dist/_chunks/index-lx7um1Yx.mjs +72 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +51 -19
- package/dist/server/index.mjs +51 -19
- package/dist/server/src/index.d.ts +6 -0
- package/dist/server/src/properties.d.ts +41 -3
- package/package.json +1 -1
- package/dist/_chunks/index-DIwNOVQ0.mjs +0 -42
- package/dist/_chunks/index-sOi9wC_y.js +0 -41
package/README.md
CHANGED
|
@@ -44,8 +44,10 @@ Société · Nombre d'employés (numberofemployees)
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
**The list narrows to the object you picked.** Point the field at the sibling
|
|
47
|
-
that holds the object
|
|
48
|
-
|
|
47
|
+
that holds the object — from the Content-Type Builder, under *HubSpot → Object
|
|
48
|
+
field* — and choosing *Contact* leaves only contact properties. The prefix
|
|
49
|
+
disappears, since it is no longer telling you anything. In `schema.json` that
|
|
50
|
+
setting reads:
|
|
49
51
|
|
|
50
52
|
```json
|
|
51
53
|
{
|
|
@@ -69,7 +71,25 @@ entry before it is written, at any depth — steps, repeatable components, dynam
|
|
|
69
71
|
zones. An invalid mapping is refused with a message that says which property and
|
|
70
72
|
why:
|
|
71
73
|
|
|
72
|
-
>
|
|
74
|
+
> Invalid HubSpot mapping — "name" exists on company, not on contact
|
|
75
|
+
|
|
76
|
+
It catches four things:
|
|
77
|
+
|
|
78
|
+
| Code | Case |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `unknown` | The property doesn't exist in the portal |
|
|
81
|
+
| `wrong-object` | It exists, but on another object |
|
|
82
|
+
| `whitespace` | It exists, but the stored value has surrounding spaces |
|
|
83
|
+
| `bad-option` | The field offers a choice the enumeration doesn't accept |
|
|
84
|
+
|
|
85
|
+
That last one closes a real hole: HubSpot refuses a value outside an
|
|
86
|
+
enumeration exactly as hard as it refuses an unknown property, so a select whose
|
|
87
|
+
choices drifted from the CRM fails at send time with nothing to show for it.
|
|
88
|
+
Point `optionsField` at the repeatable holding the choices (default `options`,
|
|
89
|
+
each `{ value?, label? }`) and they are checked too.
|
|
90
|
+
|
|
91
|
+
Each problem is also carried as a structured code in the error's `details`, so a
|
|
92
|
+
host app can localize the message instead of parsing the sentence.
|
|
73
93
|
|
|
74
94
|
### A settings screen
|
|
75
95
|
|
|
@@ -111,6 +131,7 @@ export default ({ env }) => ({
|
|
|
111
131
|
uid: "api::form.form",
|
|
112
132
|
objectField: "hsObject", // holds "contact" | "company"
|
|
113
133
|
propertyField: "hsProperty", // holds the property name
|
|
134
|
+
optionsField: "options", // optional — the field's choices
|
|
114
135
|
},
|
|
115
136
|
],
|
|
116
137
|
},
|
|
@@ -144,8 +165,16 @@ Create a **private app** in HubSpot with a read scope per object you list:
|
|
|
144
165
|
|
|
145
166
|
An object the token can't read is **skipped, not fatal**: the picker keeps
|
|
146
167
|
working for the others and explains which one is missing a scope. `oauth` is
|
|
147
|
-
worth adding too — it exposes the portal id
|
|
148
|
-
links.
|
|
168
|
+
worth adding too — it exposes the portal id *and the portal's UI host*, which
|
|
169
|
+
turn on the *view in HubSpot* links.
|
|
170
|
+
|
|
171
|
+
### Regions
|
|
172
|
+
|
|
173
|
+
The REST API is global: `api.hubapi.com` routes by token, whatever the portal's
|
|
174
|
+
hosting region. The **web app is not** — an EU-hosted portal lives on
|
|
175
|
+
`app-eu1.hubspot.com`. Deep links are built from the `uiDomain` HubSpot reports
|
|
176
|
+
for your portal rather than a hardcoded host, so they point at the right region
|
|
177
|
+
without any configuration.
|
|
149
178
|
|
|
150
179
|
The key is resolved in this order, first match wins:
|
|
151
180
|
|
package/dist/_chunks/{HubspotPropertyInput-D0gijgli.mjs → HubspotPropertyInput-C3KbDHYP.mjs}
RENAMED
|
@@ -2,7 +2,7 @@ import { jsxs, jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { Field, Flex, Loader, Combobox, ComboboxOption, Link } from "@strapi/design-system";
|
|
4
4
|
import { useFetchClient, useField } from "@strapi/strapi/admin";
|
|
5
|
-
import { P as PLUGIN_ID } from "./index-
|
|
5
|
+
import { P as PLUGIN_ID } from "./index-lx7um1Yx.mjs";
|
|
6
6
|
const LABELS = {
|
|
7
7
|
contact: "Contact",
|
|
8
8
|
company: "Société",
|
|
@@ -68,7 +68,7 @@ const HubspotPropertyInput = React.forwardRef(
|
|
|
68
68
|
}
|
|
69
69
|
return hint;
|
|
70
70
|
};
|
|
71
|
-
const crmLink = schema?.portalId && value && schema.properties.some((p) => p.name === value) ? `https
|
|
71
|
+
const crmLink = schema?.portalId && value && schema.properties.some((p) => p.name === value) ? `https://${schema.uiDomain || "app.hubspot.com"}/property-settings/${schema.portalId}/properties?search=${encodeURIComponent(value)}` : null;
|
|
72
72
|
return /* @__PURE__ */ jsxs(Field.Root, { name, error, hint: describe(), required, children: [
|
|
73
73
|
/* @__PURE__ */ jsx(Field.Label, { action: labelAction, children: label }),
|
|
74
74
|
!schema ? /* @__PURE__ */ jsx(Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsx(Loader, { small: true, children: "Chargement des propriétés HubSpot…" }) }) : schema.configured ? /* @__PURE__ */ jsx(
|
|
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
4
4
|
const React = require("react");
|
|
5
5
|
const designSystem = require("@strapi/design-system");
|
|
6
6
|
const admin = require("@strapi/strapi/admin");
|
|
7
|
-
const index = require("./index-
|
|
7
|
+
const index = require("./index-Bq1tS5h6.js");
|
|
8
8
|
function _interopNamespace(e) {
|
|
9
9
|
if (e && e.__esModule) return e;
|
|
10
10
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
@@ -88,7 +88,7 @@ const HubspotPropertyInput = React__namespace.forwardRef(
|
|
|
88
88
|
}
|
|
89
89
|
return hint;
|
|
90
90
|
};
|
|
91
|
-
const crmLink = schema?.portalId && value && schema.properties.some((p) => p.name === value) ? `https
|
|
91
|
+
const crmLink = schema?.portalId && value && schema.properties.some((p) => p.name === value) ? `https://${schema.uiDomain || "app.hubspot.com"}/property-settings/${schema.portalId}/properties?search=${encodeURIComponent(value)}` : null;
|
|
92
92
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name, error, hint: describe(), required, children: [
|
|
93
93
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { action: labelAction, children: label }),
|
|
94
94
|
!schema ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true, children: "Chargement des propriétés HubSpot…" }) }) : schema.configured ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2,7 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { Box, Loader, Flex, Typography, Badge, Field, Button } from "@strapi/design-system";
|
|
4
4
|
import { useFetchClient } from "@strapi/strapi/admin";
|
|
5
|
-
import { P as PLUGIN_ID } from "./index-
|
|
5
|
+
import { P as PLUGIN_ID } from "./index-lx7um1Yx.mjs";
|
|
6
6
|
const SOURCE_LABEL = {
|
|
7
7
|
settings: "saisie ici",
|
|
8
8
|
config: "config/plugins.ts",
|
|
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
4
4
|
const React = require("react");
|
|
5
5
|
const designSystem = require("@strapi/design-system");
|
|
6
6
|
const admin = require("@strapi/strapi/admin");
|
|
7
|
-
const index = require("./index-
|
|
7
|
+
const index = require("./index-Bq1tS5h6.js");
|
|
8
8
|
function _interopNamespace(e) {
|
|
9
9
|
if (e && e.__esModule) return e;
|
|
10
10
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const PLUGIN_ID = "hubspot";
|
|
3
|
+
const index = {
|
|
4
|
+
register(app) {
|
|
5
|
+
app.customFields.register({
|
|
6
|
+
name: "property",
|
|
7
|
+
pluginId: PLUGIN_ID,
|
|
8
|
+
type: "string",
|
|
9
|
+
intlLabel: {
|
|
10
|
+
id: `${PLUGIN_ID}.property.label`,
|
|
11
|
+
defaultMessage: "Propriété HubSpot"
|
|
12
|
+
},
|
|
13
|
+
intlDescription: {
|
|
14
|
+
id: `${PLUGIN_ID}.property.description`,
|
|
15
|
+
defaultMessage: "Choisie dans les propriétés réelles du portail"
|
|
16
|
+
},
|
|
17
|
+
components: {
|
|
18
|
+
Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-C9PpYYgj.js"))
|
|
19
|
+
},
|
|
20
|
+
// Exposed in the Content-Type Builder. Without this, `objectField` could
|
|
21
|
+
// only be set by hand-editing schema.json — which is not how anyone
|
|
22
|
+
// installing from npm adds a field, so the filtering would be unreachable.
|
|
23
|
+
options: {
|
|
24
|
+
base: [
|
|
25
|
+
{
|
|
26
|
+
sectionTitle: {
|
|
27
|
+
id: `${PLUGIN_ID}.options.section`,
|
|
28
|
+
defaultMessage: "HubSpot"
|
|
29
|
+
},
|
|
30
|
+
items: [
|
|
31
|
+
{
|
|
32
|
+
name: "options.objectField",
|
|
33
|
+
type: "text",
|
|
34
|
+
intlLabel: {
|
|
35
|
+
id: `${PLUGIN_ID}.options.objectField.label`,
|
|
36
|
+
defaultMessage: "Object field"
|
|
37
|
+
},
|
|
38
|
+
description: {
|
|
39
|
+
id: `${PLUGIN_ID}.options.objectField.description`,
|
|
40
|
+
defaultMessage: "Name of the sibling field holding the HubSpot object (e.g. hsObject). Leave empty to list every object's properties."
|
|
41
|
+
},
|
|
42
|
+
placeholder: {
|
|
43
|
+
id: `${PLUGIN_ID}.options.objectField.placeholder`,
|
|
44
|
+
defaultMessage: "hsObject"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
app.createSettingSection(
|
|
53
|
+
{
|
|
54
|
+
id: PLUGIN_ID,
|
|
55
|
+
intlLabel: { id: `${PLUGIN_ID}.settings.section`, defaultMessage: "HubSpot" }
|
|
56
|
+
},
|
|
57
|
+
[
|
|
58
|
+
{
|
|
59
|
+
intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
|
|
60
|
+
id: `${PLUGIN_ID}-settings`,
|
|
61
|
+
to: `/settings/${PLUGIN_ID}`,
|
|
62
|
+
permissions: [],
|
|
63
|
+
Component: async () => (await Promise.resolve().then(() => require("./Settings-Lj49S5fO.js"))).default
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
);
|
|
67
|
+
app.registerPlugin({ id: PLUGIN_ID, name: PLUGIN_ID });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
exports.PLUGIN_ID = PLUGIN_ID;
|
|
71
|
+
exports.index = index;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const PLUGIN_ID = "hubspot";
|
|
2
|
+
const index = {
|
|
3
|
+
register(app) {
|
|
4
|
+
app.customFields.register({
|
|
5
|
+
name: "property",
|
|
6
|
+
pluginId: PLUGIN_ID,
|
|
7
|
+
type: "string",
|
|
8
|
+
intlLabel: {
|
|
9
|
+
id: `${PLUGIN_ID}.property.label`,
|
|
10
|
+
defaultMessage: "Propriété HubSpot"
|
|
11
|
+
},
|
|
12
|
+
intlDescription: {
|
|
13
|
+
id: `${PLUGIN_ID}.property.description`,
|
|
14
|
+
defaultMessage: "Choisie dans les propriétés réelles du portail"
|
|
15
|
+
},
|
|
16
|
+
components: {
|
|
17
|
+
Input: async () => import("./HubspotPropertyInput-C3KbDHYP.mjs")
|
|
18
|
+
},
|
|
19
|
+
// Exposed in the Content-Type Builder. Without this, `objectField` could
|
|
20
|
+
// only be set by hand-editing schema.json — which is not how anyone
|
|
21
|
+
// installing from npm adds a field, so the filtering would be unreachable.
|
|
22
|
+
options: {
|
|
23
|
+
base: [
|
|
24
|
+
{
|
|
25
|
+
sectionTitle: {
|
|
26
|
+
id: `${PLUGIN_ID}.options.section`,
|
|
27
|
+
defaultMessage: "HubSpot"
|
|
28
|
+
},
|
|
29
|
+
items: [
|
|
30
|
+
{
|
|
31
|
+
name: "options.objectField",
|
|
32
|
+
type: "text",
|
|
33
|
+
intlLabel: {
|
|
34
|
+
id: `${PLUGIN_ID}.options.objectField.label`,
|
|
35
|
+
defaultMessage: "Object field"
|
|
36
|
+
},
|
|
37
|
+
description: {
|
|
38
|
+
id: `${PLUGIN_ID}.options.objectField.description`,
|
|
39
|
+
defaultMessage: "Name of the sibling field holding the HubSpot object (e.g. hsObject). Leave empty to list every object's properties."
|
|
40
|
+
},
|
|
41
|
+
placeholder: {
|
|
42
|
+
id: `${PLUGIN_ID}.options.objectField.placeholder`,
|
|
43
|
+
defaultMessage: "hsObject"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
app.createSettingSection(
|
|
52
|
+
{
|
|
53
|
+
id: PLUGIN_ID,
|
|
54
|
+
intlLabel: { id: `${PLUGIN_ID}.settings.section`, defaultMessage: "HubSpot" }
|
|
55
|
+
},
|
|
56
|
+
[
|
|
57
|
+
{
|
|
58
|
+
intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
|
|
59
|
+
id: `${PLUGIN_ID}-settings`,
|
|
60
|
+
to: `/settings/${PLUGIN_ID}`,
|
|
61
|
+
permissions: [],
|
|
62
|
+
Component: async () => (await import("./Settings-DGJ_mmlw.mjs")).default
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
);
|
|
66
|
+
app.registerPlugin({ id: PLUGIN_ID, name: PLUGIN_ID });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
export {
|
|
70
|
+
PLUGIN_ID as P,
|
|
71
|
+
index as i
|
|
72
|
+
};
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -39,12 +39,14 @@ async function fetchObject(apiKey, object) {
|
|
|
39
39
|
group: p.groupName
|
|
40
40
|
}));
|
|
41
41
|
}
|
|
42
|
-
async function
|
|
42
|
+
async function fetchAccount(apiKey) {
|
|
43
43
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
return await hsGet(
|
|
45
|
+
apiKey,
|
|
46
|
+
"/account-info/v3/details"
|
|
47
|
+
);
|
|
46
48
|
} catch {
|
|
47
|
-
return
|
|
49
|
+
return {};
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
@@ -67,23 +69,25 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
|
67
69
|
for (const entry of settled) {
|
|
68
70
|
if ("error" in entry && entry.error) {
|
|
69
71
|
unavailable.push({ object: entry.object.name, reason: entry.error });
|
|
70
|
-
strapi.log.warn(`[hubspot] ${entry.object.name}
|
|
72
|
+
strapi.log.warn(`[hubspot] ${entry.object.name} unreadable — ${entry.error}`);
|
|
71
73
|
continue;
|
|
72
74
|
}
|
|
73
75
|
available.push(entry.object.name);
|
|
74
76
|
properties.push(...entry.properties ?? []);
|
|
75
77
|
}
|
|
76
78
|
if (!available.length) {
|
|
77
|
-
throw new Error(unavailable[0]?.reason ?? "
|
|
79
|
+
throw new Error(unavailable[0]?.reason ?? "No readable object");
|
|
78
80
|
}
|
|
79
81
|
properties.sort(
|
|
80
82
|
(a, b) => a.object.localeCompare(b.object) || a.label.localeCompare(b.label)
|
|
81
83
|
);
|
|
84
|
+
const account = await fetchAccount(apiKey);
|
|
82
85
|
const schema = {
|
|
83
86
|
properties,
|
|
84
87
|
objects: available,
|
|
85
88
|
unavailable,
|
|
86
|
-
portalId:
|
|
89
|
+
portalId: account.portalId,
|
|
90
|
+
uiDomain: account.uiDomain
|
|
87
91
|
};
|
|
88
92
|
cache = { at: Date.now(), schema };
|
|
89
93
|
return schema;
|
|
@@ -96,16 +100,35 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
|
96
100
|
function clearCache() {
|
|
97
101
|
cache = null;
|
|
98
102
|
}
|
|
99
|
-
function
|
|
100
|
-
const
|
|
103
|
+
function checkMapping(properties, mapping) {
|
|
104
|
+
const { object, property } = mapping;
|
|
105
|
+
const trimmed = property.trim();
|
|
101
106
|
if (!trimmed) return null;
|
|
102
107
|
const match = properties.find((p) => p.object === object && p.name === trimmed);
|
|
103
|
-
if (match)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
if (!match) {
|
|
109
|
+
const elsewhere = properties.find((p) => p.name === trimmed);
|
|
110
|
+
return elsewhere ? { code: "wrong-object", property: trimmed, object, actualObject: elsewhere.object } : { code: "unknown", property: trimmed, object };
|
|
111
|
+
}
|
|
112
|
+
if (trimmed !== property) return { code: "whitespace", property, object };
|
|
113
|
+
if (match.type === "enumeration" && match.options.length && mapping.values?.length) {
|
|
114
|
+
const stray = mapping.values.filter((v) => v && !match.options.includes(v));
|
|
115
|
+
if (stray.length) {
|
|
116
|
+
return { code: "bad-option", property: trimmed, object, values: stray };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function describeProblem(problem) {
|
|
122
|
+
switch (problem.code) {
|
|
123
|
+
case "unknown":
|
|
124
|
+
return `"${problem.property}" does not exist on ${problem.object} in this HubSpot portal`;
|
|
125
|
+
case "wrong-object":
|
|
126
|
+
return `"${problem.property}" exists on ${problem.actualObject}, not on ${problem.object}`;
|
|
127
|
+
case "whitespace":
|
|
128
|
+
return `"${problem.property}" has surrounding whitespace`;
|
|
129
|
+
case "bad-option":
|
|
130
|
+
return `"${problem.property}" does not accept ${problem.values.map((v) => `"${v}"`).join(", ")}`;
|
|
107
131
|
}
|
|
108
|
-
return `« ${trimmed} » n'existe pas dans ce portail HubSpot`;
|
|
109
132
|
}
|
|
110
133
|
function resolveObjects(configured) {
|
|
111
134
|
if (!Array.isArray(configured) || !configured.length) {
|
|
@@ -181,7 +204,7 @@ const controllers = {
|
|
|
181
204
|
ctx.body = { configured: true, ...schema };
|
|
182
205
|
} catch (err) {
|
|
183
206
|
strapi.log.error(`[hubspot] ${err.message}`);
|
|
184
|
-
ctx.throw(502, "
|
|
207
|
+
ctx.throw(502, "Cannot reach HubSpot — check the API key.");
|
|
185
208
|
}
|
|
186
209
|
}
|
|
187
210
|
}),
|
|
@@ -227,9 +250,16 @@ function collectMappings(node, target, found = []) {
|
|
|
227
250
|
const property = obj[target.propertyField];
|
|
228
251
|
if (typeof property === "string" && property.trim()) {
|
|
229
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;
|
|
230
259
|
found.push({
|
|
231
260
|
object: typeof object === "string" && object ? object : "contact",
|
|
232
|
-
property
|
|
261
|
+
property,
|
|
262
|
+
values
|
|
233
263
|
});
|
|
234
264
|
}
|
|
235
265
|
for (const value of Object.values(obj)) collectMappings(value, target, found);
|
|
@@ -265,13 +295,15 @@ const index = {
|
|
|
265
295
|
resolveObjects(strapi.plugin("hubspot").config("objects", []))
|
|
266
296
|
);
|
|
267
297
|
} catch {
|
|
268
|
-
strapi.log.warn("[hubspot]
|
|
298
|
+
strapi.log.warn("[hubspot] schema unavailable — validation skipped");
|
|
269
299
|
return next();
|
|
270
300
|
}
|
|
271
|
-
const problems = mappings.map((m) =>
|
|
301
|
+
const problems = mappings.map((m) => checkMapping(schema.properties, m)).filter((p) => Boolean(p));
|
|
272
302
|
if (problems.length) {
|
|
303
|
+
const sentences = [...new Set(problems.map(describeProblem))];
|
|
273
304
|
throw new utils.errors.ValidationError(
|
|
274
|
-
`
|
|
305
|
+
`Invalid HubSpot mapping — ${sentences.join("; ")}`,
|
|
306
|
+
{ problems }
|
|
275
307
|
);
|
|
276
308
|
}
|
|
277
309
|
return next();
|
package/dist/server/index.mjs
CHANGED
|
@@ -38,12 +38,14 @@ async function fetchObject(apiKey, object) {
|
|
|
38
38
|
group: p.groupName
|
|
39
39
|
}));
|
|
40
40
|
}
|
|
41
|
-
async function
|
|
41
|
+
async function fetchAccount(apiKey) {
|
|
42
42
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
return await hsGet(
|
|
44
|
+
apiKey,
|
|
45
|
+
"/account-info/v3/details"
|
|
46
|
+
);
|
|
45
47
|
} catch {
|
|
46
|
-
return
|
|
48
|
+
return {};
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
@@ -66,23 +68,25 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
|
66
68
|
for (const entry of settled) {
|
|
67
69
|
if ("error" in entry && entry.error) {
|
|
68
70
|
unavailable.push({ object: entry.object.name, reason: entry.error });
|
|
69
|
-
strapi.log.warn(`[hubspot] ${entry.object.name}
|
|
71
|
+
strapi.log.warn(`[hubspot] ${entry.object.name} unreadable — ${entry.error}`);
|
|
70
72
|
continue;
|
|
71
73
|
}
|
|
72
74
|
available.push(entry.object.name);
|
|
73
75
|
properties.push(...entry.properties ?? []);
|
|
74
76
|
}
|
|
75
77
|
if (!available.length) {
|
|
76
|
-
throw new Error(unavailable[0]?.reason ?? "
|
|
78
|
+
throw new Error(unavailable[0]?.reason ?? "No readable object");
|
|
77
79
|
}
|
|
78
80
|
properties.sort(
|
|
79
81
|
(a, b) => a.object.localeCompare(b.object) || a.label.localeCompare(b.label)
|
|
80
82
|
);
|
|
83
|
+
const account = await fetchAccount(apiKey);
|
|
81
84
|
const schema = {
|
|
82
85
|
properties,
|
|
83
86
|
objects: available,
|
|
84
87
|
unavailable,
|
|
85
|
-
portalId:
|
|
88
|
+
portalId: account.portalId,
|
|
89
|
+
uiDomain: account.uiDomain
|
|
86
90
|
};
|
|
87
91
|
cache = { at: Date.now(), schema };
|
|
88
92
|
return schema;
|
|
@@ -95,16 +99,35 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
|
95
99
|
function clearCache() {
|
|
96
100
|
cache = null;
|
|
97
101
|
}
|
|
98
|
-
function
|
|
99
|
-
const
|
|
102
|
+
function checkMapping(properties, mapping) {
|
|
103
|
+
const { object, property } = mapping;
|
|
104
|
+
const trimmed = property.trim();
|
|
100
105
|
if (!trimmed) return null;
|
|
101
106
|
const match = properties.find((p) => p.object === object && p.name === trimmed);
|
|
102
|
-
if (match)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
if (!match) {
|
|
108
|
+
const elsewhere = properties.find((p) => p.name === trimmed);
|
|
109
|
+
return elsewhere ? { code: "wrong-object", property: trimmed, object, actualObject: elsewhere.object } : { code: "unknown", property: trimmed, object };
|
|
110
|
+
}
|
|
111
|
+
if (trimmed !== property) return { code: "whitespace", property, object };
|
|
112
|
+
if (match.type === "enumeration" && match.options.length && mapping.values?.length) {
|
|
113
|
+
const stray = mapping.values.filter((v) => v && !match.options.includes(v));
|
|
114
|
+
if (stray.length) {
|
|
115
|
+
return { code: "bad-option", property: trimmed, object, values: stray };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function describeProblem(problem) {
|
|
121
|
+
switch (problem.code) {
|
|
122
|
+
case "unknown":
|
|
123
|
+
return `"${problem.property}" does not exist on ${problem.object} in this HubSpot portal`;
|
|
124
|
+
case "wrong-object":
|
|
125
|
+
return `"${problem.property}" exists on ${problem.actualObject}, not on ${problem.object}`;
|
|
126
|
+
case "whitespace":
|
|
127
|
+
return `"${problem.property}" has surrounding whitespace`;
|
|
128
|
+
case "bad-option":
|
|
129
|
+
return `"${problem.property}" does not accept ${problem.values.map((v) => `"${v}"`).join(", ")}`;
|
|
106
130
|
}
|
|
107
|
-
return `« ${trimmed} » n'existe pas dans ce portail HubSpot`;
|
|
108
131
|
}
|
|
109
132
|
function resolveObjects(configured) {
|
|
110
133
|
if (!Array.isArray(configured) || !configured.length) {
|
|
@@ -180,7 +203,7 @@ const controllers = {
|
|
|
180
203
|
ctx.body = { configured: true, ...schema };
|
|
181
204
|
} catch (err) {
|
|
182
205
|
strapi.log.error(`[hubspot] ${err.message}`);
|
|
183
|
-
ctx.throw(502, "
|
|
206
|
+
ctx.throw(502, "Cannot reach HubSpot — check the API key.");
|
|
184
207
|
}
|
|
185
208
|
}
|
|
186
209
|
}),
|
|
@@ -226,9 +249,16 @@ function collectMappings(node, target, found = []) {
|
|
|
226
249
|
const property = obj[target.propertyField];
|
|
227
250
|
if (typeof property === "string" && property.trim()) {
|
|
228
251
|
const object = obj[target.objectField];
|
|
252
|
+
const rawOptions = obj[target.optionsField || "options"];
|
|
253
|
+
const values = Array.isArray(rawOptions) ? rawOptions.map((o) => {
|
|
254
|
+
const opt = o ?? {};
|
|
255
|
+
const v = typeof opt.value === "string" && opt.value.trim() ? opt.value : opt.label;
|
|
256
|
+
return typeof v === "string" ? v.trim() : "";
|
|
257
|
+
}).filter(Boolean) : void 0;
|
|
229
258
|
found.push({
|
|
230
259
|
object: typeof object === "string" && object ? object : "contact",
|
|
231
|
-
property
|
|
260
|
+
property,
|
|
261
|
+
values
|
|
232
262
|
});
|
|
233
263
|
}
|
|
234
264
|
for (const value of Object.values(obj)) collectMappings(value, target, found);
|
|
@@ -264,13 +294,15 @@ const index = {
|
|
|
264
294
|
resolveObjects(strapi.plugin("hubspot").config("objects", []))
|
|
265
295
|
);
|
|
266
296
|
} catch {
|
|
267
|
-
strapi.log.warn("[hubspot]
|
|
297
|
+
strapi.log.warn("[hubspot] schema unavailable — validation skipped");
|
|
268
298
|
return next();
|
|
269
299
|
}
|
|
270
|
-
const problems = mappings.map((m) =>
|
|
300
|
+
const problems = mappings.map((m) => checkMapping(schema.properties, m)).filter((p) => Boolean(p));
|
|
271
301
|
if (problems.length) {
|
|
302
|
+
const sentences = [...new Set(problems.map(describeProblem))];
|
|
272
303
|
throw new errors.ValidationError(
|
|
273
|
-
`
|
|
304
|
+
`Invalid HubSpot mapping — ${sentences.join("; ")}`,
|
|
305
|
+
{ problems }
|
|
274
306
|
);
|
|
275
307
|
}
|
|
276
308
|
return next();
|
|
@@ -18,6 +18,12 @@ interface ValidateTarget {
|
|
|
18
18
|
uid: string;
|
|
19
19
|
objectField: string;
|
|
20
20
|
propertyField: string;
|
|
21
|
+
/**
|
|
22
|
+
* Repeatable holding the values a field can submit (default `options`), each
|
|
23
|
+
* `{ value?, label? }`. Checked against enumeration properties: a select whose
|
|
24
|
+
* choices drifted from HubSpot fails exactly like an unknown property.
|
|
25
|
+
*/
|
|
26
|
+
optionsField?: string;
|
|
21
27
|
}
|
|
22
28
|
declare const _default: {
|
|
23
29
|
config: {
|
|
@@ -29,6 +29,13 @@ export interface Schema {
|
|
|
29
29
|
}[];
|
|
30
30
|
/** Enables deep links into the CRM; derived from the token, never configured. */
|
|
31
31
|
portalId?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Region-specific UI host for this portal (`app-eu1.hubspot.com`…). The REST
|
|
34
|
+
* API is global — `api.hubapi.com` routes by token — but the web app is not,
|
|
35
|
+
* so a link built on `app.hubspot.com` lands on the wrong host for any portal
|
|
36
|
+
* hosted outside NA.
|
|
37
|
+
*/
|
|
38
|
+
uiDomain?: string;
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
34
41
|
* Writable properties of every configured object, cached and de-duplicated
|
|
@@ -43,10 +50,41 @@ export declare function loadSchema(strapi: Core.Strapi, apiKey: string, objects:
|
|
|
43
50
|
}): Promise<Schema>;
|
|
44
51
|
/** Drop the cache — used after the API key or the object list changes. */
|
|
45
52
|
export declare function clearCache(): void;
|
|
53
|
+
/** A mapping the portal would reject, and why. */
|
|
54
|
+
export type Problem = {
|
|
55
|
+
code: "unknown";
|
|
56
|
+
property: string;
|
|
57
|
+
object: string;
|
|
58
|
+
} | {
|
|
59
|
+
code: "wrong-object";
|
|
60
|
+
property: string;
|
|
61
|
+
object: string;
|
|
62
|
+
actualObject: string;
|
|
63
|
+
} | {
|
|
64
|
+
code: "whitespace";
|
|
65
|
+
property: string;
|
|
66
|
+
object: string;
|
|
67
|
+
} | {
|
|
68
|
+
code: "bad-option";
|
|
69
|
+
property: string;
|
|
70
|
+
object: string;
|
|
71
|
+
values: string[];
|
|
72
|
+
};
|
|
73
|
+
export interface Mapping {
|
|
74
|
+
object: string;
|
|
75
|
+
property: string;
|
|
76
|
+
/** Values the field can submit — checked against enumeration properties. */
|
|
77
|
+
values?: string[];
|
|
78
|
+
}
|
|
46
79
|
/**
|
|
47
|
-
* Why a
|
|
48
|
-
*
|
|
80
|
+
* Why a mapping can't be written, or `null` when it's fine.
|
|
81
|
+
*
|
|
82
|
+
* Returns a structured code rather than a sentence: the message is rendered by
|
|
83
|
+
* the caller, so the wording stays in one place and stays translatable. The
|
|
84
|
+
* codes are also carried in the ValidationError details, for anyone localizing.
|
|
49
85
|
*/
|
|
50
|
-
export declare function
|
|
86
|
+
export declare function checkMapping(properties: HsProperty[], mapping: Mapping): Problem | null;
|
|
87
|
+
/** English rendering of a problem — the package's language. */
|
|
88
|
+
export declare function describeProblem(problem: Problem): string;
|
|
51
89
|
/** Resolve the configured object list, accepting names or full definitions. */
|
|
52
90
|
export declare function resolveObjects(configured: unknown): HsObjectDef[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-hubspot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "HubSpot integration for Strapi — pick CRM properties from a searchable list filtered by object, and catch bad mappings before they reach the API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
const PLUGIN_ID = "hubspot";
|
|
2
|
-
const index = {
|
|
3
|
-
register(app) {
|
|
4
|
-
app.customFields.register({
|
|
5
|
-
name: "property",
|
|
6
|
-
pluginId: PLUGIN_ID,
|
|
7
|
-
type: "string",
|
|
8
|
-
intlLabel: {
|
|
9
|
-
id: `${PLUGIN_ID}.property.label`,
|
|
10
|
-
defaultMessage: "Propriété HubSpot"
|
|
11
|
-
},
|
|
12
|
-
intlDescription: {
|
|
13
|
-
id: `${PLUGIN_ID}.property.description`,
|
|
14
|
-
defaultMessage: "Choisie dans les propriétés réelles du portail"
|
|
15
|
-
},
|
|
16
|
-
components: {
|
|
17
|
-
Input: async () => import("./HubspotPropertyInput-D0gijgli.mjs")
|
|
18
|
-
},
|
|
19
|
-
options: {}
|
|
20
|
-
});
|
|
21
|
-
app.createSettingSection(
|
|
22
|
-
{
|
|
23
|
-
id: PLUGIN_ID,
|
|
24
|
-
intlLabel: { id: `${PLUGIN_ID}.settings.section`, defaultMessage: "HubSpot" }
|
|
25
|
-
},
|
|
26
|
-
[
|
|
27
|
-
{
|
|
28
|
-
intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
|
|
29
|
-
id: `${PLUGIN_ID}-settings`,
|
|
30
|
-
to: `/settings/${PLUGIN_ID}`,
|
|
31
|
-
permissions: [],
|
|
32
|
-
Component: async () => (await import("./Settings-DuYUpD8L.mjs")).default
|
|
33
|
-
}
|
|
34
|
-
]
|
|
35
|
-
);
|
|
36
|
-
app.registerPlugin({ id: PLUGIN_ID, name: PLUGIN_ID });
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
export {
|
|
40
|
-
PLUGIN_ID as P,
|
|
41
|
-
index as i
|
|
42
|
-
};
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const PLUGIN_ID = "hubspot";
|
|
3
|
-
const index = {
|
|
4
|
-
register(app) {
|
|
5
|
-
app.customFields.register({
|
|
6
|
-
name: "property",
|
|
7
|
-
pluginId: PLUGIN_ID,
|
|
8
|
-
type: "string",
|
|
9
|
-
intlLabel: {
|
|
10
|
-
id: `${PLUGIN_ID}.property.label`,
|
|
11
|
-
defaultMessage: "Propriété HubSpot"
|
|
12
|
-
},
|
|
13
|
-
intlDescription: {
|
|
14
|
-
id: `${PLUGIN_ID}.property.description`,
|
|
15
|
-
defaultMessage: "Choisie dans les propriétés réelles du portail"
|
|
16
|
-
},
|
|
17
|
-
components: {
|
|
18
|
-
Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-BcdqfAfv.js"))
|
|
19
|
-
},
|
|
20
|
-
options: {}
|
|
21
|
-
});
|
|
22
|
-
app.createSettingSection(
|
|
23
|
-
{
|
|
24
|
-
id: PLUGIN_ID,
|
|
25
|
-
intlLabel: { id: `${PLUGIN_ID}.settings.section`, defaultMessage: "HubSpot" }
|
|
26
|
-
},
|
|
27
|
-
[
|
|
28
|
-
{
|
|
29
|
-
intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
|
|
30
|
-
id: `${PLUGIN_ID}-settings`,
|
|
31
|
-
to: `/settings/${PLUGIN_ID}`,
|
|
32
|
-
permissions: [],
|
|
33
|
-
Component: async () => (await Promise.resolve().then(() => require("./Settings-DedA0z6w.js"))).default
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
);
|
|
37
|
-
app.registerPlugin({ id: PLUGIN_ID, name: PLUGIN_ID });
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
exports.PLUGIN_ID = PLUGIN_ID;
|
|
41
|
-
exports.index = index;
|