twenty-app-intake 0.2.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/manifest.json +1466 -0
- package/package.json +56 -0
- package/public/logo.svg +20 -0
- package/src/front-components/dashboard.mjs +21994 -0
- package/src/front-components/dashboard.mjs.map +7 -0
- package/src/front-components/settings-panel.mjs +22150 -0
- package/src/front-components/settings-panel.mjs.map +7 -0
- package/src/front-components/source-panel.mjs +22010 -0
- package/src/front-components/source-panel.mjs.map +7 -0
- package/src/logic-functions/health.mjs +118 -0
- package/src/logic-functions/health.mjs.map +7 -0
- package/src/logic-functions/register.mjs +183 -0
- package/src/logic-functions/register.mjs.map +7 -0
- package/src/logic-functions/retry.mjs +1244 -0
- package/src/logic-functions/retry.mjs.map +7 -0
- package/src/logic-functions/test-ingest.mjs +549 -0
- package/src/logic-functions/test-ingest.mjs.map +7 -0
- package/src/logic-functions/webhook.mjs +1213 -0
- package/src/logic-functions/webhook.mjs.map +7 -0
- package/src/post-install.mjs +181 -0
- package/src/post-install.mjs.map +7 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'module';
|
|
2
|
+
const require = __createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// twenty-sdk-define-stub:__twenty-sdk-define-stub__
|
|
5
|
+
var __defineFactoryStub = (config) => ({
|
|
6
|
+
success: true,
|
|
7
|
+
config,
|
|
8
|
+
errors: []
|
|
9
|
+
});
|
|
10
|
+
var __anyHandler = {
|
|
11
|
+
get(_target, prop) {
|
|
12
|
+
if (prop === "__esModule") return true;
|
|
13
|
+
if (prop === Symbol.toPrimitive) return () => "";
|
|
14
|
+
if (typeof prop === "symbol") return void 0;
|
|
15
|
+
return new Proxy(() => void 0, __anyHandler);
|
|
16
|
+
},
|
|
17
|
+
apply() {
|
|
18
|
+
return new Proxy(() => void 0, __anyHandler);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var __anyStub = new Proxy(() => void 0, __anyHandler);
|
|
22
|
+
var defineLogicFunction = __defineFactoryStub;
|
|
23
|
+
|
|
24
|
+
// src/lib/flattener.ts
|
|
25
|
+
var MAX_DEPTH = 8;
|
|
26
|
+
function flatten(input, prefix = "", depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
27
|
+
if (depth > MAX_DEPTH) return {};
|
|
28
|
+
if (input === null || input === void 0) return {};
|
|
29
|
+
if (typeof input !== "object") {
|
|
30
|
+
return prefix ? { [prefix]: input } : {};
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(input)) {
|
|
33
|
+
return prefix ? { [prefix]: input } : {};
|
|
34
|
+
}
|
|
35
|
+
const obj = input;
|
|
36
|
+
if (seen.has(obj)) return {};
|
|
37
|
+
seen.add(obj);
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
40
|
+
const newKey = prefix ? `${prefix}${toPascalSegment(key)}` : toCamelSegment(key);
|
|
41
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
42
|
+
const nested = flatten(value, newKey, depth + 1, seen);
|
|
43
|
+
Object.assign(result, nested);
|
|
44
|
+
} else {
|
|
45
|
+
result[newKey] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
function toCamelSegment(s) {
|
|
51
|
+
return s.replace(/[-_]([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
|
|
52
|
+
}
|
|
53
|
+
function toPascalSegment(s) {
|
|
54
|
+
const camel = toCamelSegment(s);
|
|
55
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
56
|
+
}
|
|
57
|
+
function detectStructure(raw) {
|
|
58
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
59
|
+
return { type: "flat", data: {} };
|
|
60
|
+
}
|
|
61
|
+
const obj = raw;
|
|
62
|
+
const hasObjectCompany = "company" in obj && typeof obj["company"] === "object" && obj["company"] !== null && !Array.isArray(obj["company"]);
|
|
63
|
+
const hasObjectPerson = "person" in obj && typeof obj["person"] === "object" && obj["person"] !== null && !Array.isArray(obj["person"]);
|
|
64
|
+
if (!hasObjectCompany && !hasObjectPerson) {
|
|
65
|
+
return { type: "flat", data: flatten(obj) };
|
|
66
|
+
}
|
|
67
|
+
const company = hasObjectCompany ? obj["company"] : null;
|
|
68
|
+
const person = hasObjectPerson ? obj["person"] : null;
|
|
69
|
+
const extra = {};
|
|
70
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
71
|
+
if (k === "company" || k === "person") continue;
|
|
72
|
+
const nested = flatten({ [k]: v });
|
|
73
|
+
Object.assign(extra, nested);
|
|
74
|
+
}
|
|
75
|
+
return { type: "structured", company, person, extra };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/constants/field-map.ts
|
|
79
|
+
function normaliseKey(key) {
|
|
80
|
+
return key.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/[-\s.]/g, "_").replace(/_{2,}/g, "_");
|
|
81
|
+
}
|
|
82
|
+
var FIELD_MAP = {
|
|
83
|
+
// ── Phone ─────────────────────────────────────────────────────────────────
|
|
84
|
+
phone: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
85
|
+
phone_number: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
86
|
+
tel: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
87
|
+
telephone: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
88
|
+
mobile: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
89
|
+
mobile_number: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
90
|
+
cell: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
91
|
+
cell_phone: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
92
|
+
contact_phone: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
93
|
+
phone1: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
94
|
+
primary_phone: { canonicalName: "phone", twentyType: "PHONES", action: "field" },
|
|
95
|
+
// ── Email ─────────────────────────────────────────────────────────────────
|
|
96
|
+
email: { canonicalName: "email", twentyType: "EMAILS", action: "field" },
|
|
97
|
+
email_address: { canonicalName: "email", twentyType: "EMAILS", action: "field" },
|
|
98
|
+
e_mail: { canonicalName: "email", twentyType: "EMAILS", action: "field" },
|
|
99
|
+
contact_email: { canonicalName: "email", twentyType: "EMAILS", action: "field" },
|
|
100
|
+
email1: { canonicalName: "email", twentyType: "EMAILS", action: "field" },
|
|
101
|
+
user_email: { canonicalName: "email", twentyType: "EMAILS", action: "field" },
|
|
102
|
+
primary_email: { canonicalName: "email", twentyType: "EMAILS", action: "field" },
|
|
103
|
+
// ── First name ────────────────────────────────────────────────────────────
|
|
104
|
+
first_name: { canonicalName: "firstName", twentyType: "TEXT", action: "field" },
|
|
105
|
+
fname: { canonicalName: "firstName", twentyType: "TEXT", action: "field" },
|
|
106
|
+
given_name: { canonicalName: "firstName", twentyType: "TEXT", action: "field" },
|
|
107
|
+
forename: { canonicalName: "firstName", twentyType: "TEXT", action: "field" },
|
|
108
|
+
// ── Last name ─────────────────────────────────────────────────────────────
|
|
109
|
+
last_name: { canonicalName: "lastName", twentyType: "TEXT", action: "field" },
|
|
110
|
+
lname: { canonicalName: "lastName", twentyType: "TEXT", action: "field" },
|
|
111
|
+
surname: { canonicalName: "lastName", twentyType: "TEXT", action: "field" },
|
|
112
|
+
family_name: { canonicalName: "lastName", twentyType: "TEXT", action: "field" },
|
|
113
|
+
// ── Full name (will be split on ingest) ───────────────────────────────────
|
|
114
|
+
name: { canonicalName: "fullName", twentyType: "FULL_NAME", action: "field" },
|
|
115
|
+
full_name: { canonicalName: "fullName", twentyType: "FULL_NAME", action: "field" },
|
|
116
|
+
contact_name: { canonicalName: "fullName", twentyType: "FULL_NAME", action: "field" },
|
|
117
|
+
your_name: { canonicalName: "fullName", twentyType: "FULL_NAME", action: "field" },
|
|
118
|
+
// ── Company ───────────────────────────────────────────────────────────────
|
|
119
|
+
company: { canonicalName: "companyName", twentyType: "TEXT", action: "field" },
|
|
120
|
+
company_name: { canonicalName: "companyName", twentyType: "TEXT", action: "field" },
|
|
121
|
+
business: { canonicalName: "companyName", twentyType: "TEXT", action: "field" },
|
|
122
|
+
business_name: { canonicalName: "companyName", twentyType: "TEXT", action: "field" },
|
|
123
|
+
organization: { canonicalName: "companyName", twentyType: "TEXT", action: "field" },
|
|
124
|
+
organisation: { canonicalName: "companyName", twentyType: "TEXT", action: "field" },
|
|
125
|
+
employer: { canonicalName: "companyName", twentyType: "TEXT", action: "field" },
|
|
126
|
+
firm: { canonicalName: "companyName", twentyType: "TEXT", action: "field" },
|
|
127
|
+
// ── Website / Domain ──────────────────────────────────────────────────────
|
|
128
|
+
website: { canonicalName: "domainName", twentyType: "LINKS", action: "field" },
|
|
129
|
+
website_url: { canonicalName: "domainName", twentyType: "LINKS", action: "field" },
|
|
130
|
+
url: { canonicalName: "domainName", twentyType: "LINKS", action: "field" },
|
|
131
|
+
domain: { canonicalName: "domainName", twentyType: "LINKS", action: "field" },
|
|
132
|
+
homepage: { canonicalName: "domainName", twentyType: "LINKS", action: "field" },
|
|
133
|
+
web: { canonicalName: "domainName", twentyType: "LINKS", action: "field" },
|
|
134
|
+
site: { canonicalName: "domainName", twentyType: "LINKS", action: "field" },
|
|
135
|
+
site_url: { canonicalName: "domainName", twentyType: "LINKS", action: "field" },
|
|
136
|
+
// ── Address ───────────────────────────────────────────────────────────────
|
|
137
|
+
street: { canonicalName: "addressStreet1", twentyType: "TEXT", action: "field" },
|
|
138
|
+
street_address: { canonicalName: "addressStreet1", twentyType: "TEXT", action: "field" },
|
|
139
|
+
address_line_1: { canonicalName: "addressStreet1", twentyType: "TEXT", action: "field" },
|
|
140
|
+
address1: { canonicalName: "addressStreet1", twentyType: "TEXT", action: "field" },
|
|
141
|
+
address_line_2: { canonicalName: "addressStreet2", twentyType: "TEXT", action: "field" },
|
|
142
|
+
address2: { canonicalName: "addressStreet2", twentyType: "TEXT", action: "field" },
|
|
143
|
+
city: { canonicalName: "addressCity", twentyType: "TEXT", action: "field" },
|
|
144
|
+
address_city: { canonicalName: "addressCity", twentyType: "TEXT", action: "field" },
|
|
145
|
+
town: { canonicalName: "addressCity", twentyType: "TEXT", action: "field" },
|
|
146
|
+
state: { canonicalName: "addressState", twentyType: "TEXT", action: "field" },
|
|
147
|
+
province: { canonicalName: "addressState", twentyType: "TEXT", action: "field" },
|
|
148
|
+
region: { canonicalName: "addressState", twentyType: "TEXT", action: "field" },
|
|
149
|
+
address_state: { canonicalName: "addressState", twentyType: "TEXT", action: "field" },
|
|
150
|
+
zip: { canonicalName: "addressPostcode", twentyType: "TEXT", action: "field" },
|
|
151
|
+
zip_code: { canonicalName: "addressPostcode", twentyType: "TEXT", action: "field" },
|
|
152
|
+
postal_code: { canonicalName: "addressPostcode", twentyType: "TEXT", action: "field" },
|
|
153
|
+
postcode: { canonicalName: "addressPostcode", twentyType: "TEXT", action: "field" },
|
|
154
|
+
country: { canonicalName: "addressCountry", twentyType: "TEXT", action: "field" },
|
|
155
|
+
address_country: { canonicalName: "addressCountry", twentyType: "TEXT", action: "field" },
|
|
156
|
+
// ── Social ────────────────────────────────────────────────────────────────
|
|
157
|
+
linkedin: { canonicalName: "linkedInLink", twentyType: "LINKS", action: "field" },
|
|
158
|
+
linkedin_url: { canonicalName: "linkedInLink", twentyType: "LINKS", action: "field" },
|
|
159
|
+
linkedin_profile: { canonicalName: "linkedInLink", twentyType: "LINKS", action: "field" },
|
|
160
|
+
twitter: { canonicalName: "xLink", twentyType: "LINKS", action: "field" },
|
|
161
|
+
twitter_url: { canonicalName: "xLink", twentyType: "LINKS", action: "field" },
|
|
162
|
+
x_profile: { canonicalName: "xLink", twentyType: "LINKS", action: "field" },
|
|
163
|
+
// ── Pipeline-specific (Google / business intel) ───────────────────────────
|
|
164
|
+
google_places_url: { canonicalName: "extGooglePlacesUrl", twentyType: "LINKS", action: "field" },
|
|
165
|
+
places_url: { canonicalName: "extGooglePlacesUrl", twentyType: "LINKS", action: "field" },
|
|
166
|
+
google_maps_url: { canonicalName: "extGooglePlacesUrl", twentyType: "LINKS", action: "field" },
|
|
167
|
+
maps_url: { canonicalName: "extGooglePlacesUrl", twentyType: "LINKS", action: "field" },
|
|
168
|
+
rating: { canonicalName: "extGoogleRating", twentyType: "NUMBER", action: "field" },
|
|
169
|
+
google_rating: { canonicalName: "extGoogleRating", twentyType: "NUMBER", action: "field" },
|
|
170
|
+
stars: { canonicalName: "extGoogleRating", twentyType: "NUMBER", action: "field" },
|
|
171
|
+
review_count: { canonicalName: "extReviewCount", twentyType: "NUMBER", action: "field" },
|
|
172
|
+
reviews_count: { canonicalName: "extReviewCount", twentyType: "NUMBER", action: "field" },
|
|
173
|
+
total_reviews: { canonicalName: "extReviewCount", twentyType: "NUMBER", action: "field" },
|
|
174
|
+
num_reviews: { canonicalName: "extReviewCount", twentyType: "NUMBER", action: "field" },
|
|
175
|
+
marketing_score: { canonicalName: "extMarketingScore", twentyType: "NUMBER", action: "field" },
|
|
176
|
+
lead_score: { canonicalName: "extLeadScore", twentyType: "NUMBER", action: "field" },
|
|
177
|
+
analyzed_url: { canonicalName: "extAnalyzedUrl", twentyType: "LINKS", action: "field" },
|
|
178
|
+
// ── UTM — always captured in note ────────────────────────────────────────
|
|
179
|
+
utm_source: { canonicalName: "utm_source", twentyType: "TEXT", action: "note" },
|
|
180
|
+
utm_medium: { canonicalName: "utm_medium", twentyType: "TEXT", action: "note" },
|
|
181
|
+
utm_campaign: { canonicalName: "utm_campaign", twentyType: "TEXT", action: "note" },
|
|
182
|
+
utm_content: { canonicalName: "utm_content", twentyType: "TEXT", action: "note" },
|
|
183
|
+
utm_term: { canonicalName: "utm_term", twentyType: "TEXT", action: "note" },
|
|
184
|
+
ref: { canonicalName: "ref", twentyType: "TEXT", action: "note" },
|
|
185
|
+
referrer: { canonicalName: "referrer", twentyType: "TEXT", action: "note" },
|
|
186
|
+
source_page: { canonicalName: "source_page", twentyType: "TEXT", action: "note" },
|
|
187
|
+
landing_page: { canonicalName: "landing_page", twentyType: "TEXT", action: "note" },
|
|
188
|
+
// ── Known note fields — always prose ─────────────────────────────────────
|
|
189
|
+
message: { canonicalName: "message", twentyType: "RICH_TEXT", action: "note" },
|
|
190
|
+
description: { canonicalName: "description", twentyType: "RICH_TEXT", action: "note" },
|
|
191
|
+
analysis: { canonicalName: "analysis", twentyType: "RICH_TEXT", action: "note" },
|
|
192
|
+
notes: { canonicalName: "notes", twentyType: "RICH_TEXT", action: "note" },
|
|
193
|
+
comments: { canonicalName: "comments", twentyType: "RICH_TEXT", action: "note" },
|
|
194
|
+
summary: { canonicalName: "summary", twentyType: "RICH_TEXT", action: "note" },
|
|
195
|
+
inquiry: { canonicalName: "inquiry", twentyType: "RICH_TEXT", action: "note" },
|
|
196
|
+
body: { canonicalName: "body", twentyType: "RICH_TEXT", action: "note" },
|
|
197
|
+
details: { canonicalName: "details", twentyType: "RICH_TEXT", action: "note" },
|
|
198
|
+
content: { canonicalName: "content", twentyType: "RICH_TEXT", action: "note" },
|
|
199
|
+
text: { canonicalName: "text", twentyType: "RICH_TEXT", action: "note" },
|
|
200
|
+
feedback: { canonicalName: "feedback", twentyType: "RICH_TEXT", action: "note" },
|
|
201
|
+
about: { canonicalName: "about", twentyType: "RICH_TEXT", action: "note" },
|
|
202
|
+
additional_info: { canonicalName: "additional_info", twentyType: "RICH_TEXT", action: "note" },
|
|
203
|
+
// ── System / internal — skip entirely ────────────────────────────────────
|
|
204
|
+
_id: { canonicalName: "_id", twentyType: "TEXT", action: "skip" },
|
|
205
|
+
__v: { canonicalName: "__v", twentyType: "TEXT", action: "skip" },
|
|
206
|
+
created_at: { canonicalName: "createdAt", twentyType: "DATE_TIME", action: "skip" },
|
|
207
|
+
updated_at: { canonicalName: "updatedAt", twentyType: "DATE_TIME", action: "skip" },
|
|
208
|
+
form_id: { canonicalName: "form_id", twentyType: "TEXT", action: "skip" },
|
|
209
|
+
submission_id: { canonicalName: "submission_id", twentyType: "TEXT", action: "skip" },
|
|
210
|
+
token: { canonicalName: "token", twentyType: "TEXT", action: "skip" },
|
|
211
|
+
password: { canonicalName: "password", twentyType: "TEXT", action: "skip" },
|
|
212
|
+
secret: { canonicalName: "secret", twentyType: "TEXT", action: "skip" }
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/lib/type-detector.ts
|
|
216
|
+
var URL_RE = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
|
|
217
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/;
|
|
218
|
+
var PHONE_RE = /^[+]?[(]?[0-9]{1,4}[)]?[-\s.]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{3,9}([-\s.][0-9]{1,9})?$/;
|
|
219
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?)?$/;
|
|
220
|
+
function detectType(value) {
|
|
221
|
+
if (value === null || value === void 0) return "TEXT";
|
|
222
|
+
if (typeof value === "boolean") return "BOOLEAN";
|
|
223
|
+
if (typeof value === "number") return "NUMBER";
|
|
224
|
+
if (typeof value === "string") {
|
|
225
|
+
const s = value.trim();
|
|
226
|
+
if (!s) return "TEXT";
|
|
227
|
+
if (URL_RE.test(s)) return "LINKS";
|
|
228
|
+
if (EMAIL_RE.test(s)) return "EMAILS";
|
|
229
|
+
if (PHONE_RE.test(s)) return "PHONES";
|
|
230
|
+
if (DATE_RE.test(s)) return "DATE_TIME";
|
|
231
|
+
if (!isNaN(Number(s)) && s !== "") return "NUMBER";
|
|
232
|
+
return "TEXT";
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(value)) return "RAW_JSON";
|
|
235
|
+
if (typeof value === "object") return "RAW_JSON";
|
|
236
|
+
return "TEXT";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/lib/classifier.ts
|
|
240
|
+
var PROSE_KEYS = /* @__PURE__ */ new Set([
|
|
241
|
+
"message",
|
|
242
|
+
"description",
|
|
243
|
+
"analysis",
|
|
244
|
+
"notes",
|
|
245
|
+
"comments",
|
|
246
|
+
"summary",
|
|
247
|
+
"inquiry",
|
|
248
|
+
"body",
|
|
249
|
+
"details",
|
|
250
|
+
"content",
|
|
251
|
+
"text",
|
|
252
|
+
"feedback",
|
|
253
|
+
"about",
|
|
254
|
+
"additional_info",
|
|
255
|
+
"info",
|
|
256
|
+
"context",
|
|
257
|
+
"requirements",
|
|
258
|
+
"goals",
|
|
259
|
+
"challenges",
|
|
260
|
+
"question",
|
|
261
|
+
"request"
|
|
262
|
+
]);
|
|
263
|
+
var NOTE_KEY_PREFIXES = ["utm_", "ga_", "fb_", "gclid", "msclkid"];
|
|
264
|
+
var SENTENCE_RE = /[.!?]\s+[A-Z]/g;
|
|
265
|
+
var LINE_BREAK_RE = /\n|\r/;
|
|
266
|
+
function classify(key, value) {
|
|
267
|
+
const normKey = key.toLowerCase().replace(/[-\s]/g, "_");
|
|
268
|
+
if (Array.isArray(value)) return "note";
|
|
269
|
+
if (value !== null && typeof value === "object") return "note";
|
|
270
|
+
if (PROSE_KEYS.has(normKey)) return "note";
|
|
271
|
+
if (NOTE_KEY_PREFIXES.some((p) => normKey.startsWith(p))) return "note";
|
|
272
|
+
if (typeof value === "string") {
|
|
273
|
+
if (LINE_BREAK_RE.test(value)) return "note";
|
|
274
|
+
const matches = value.match(SENTENCE_RE);
|
|
275
|
+
if (matches && matches.length >= 2) return "note";
|
|
276
|
+
}
|
|
277
|
+
return "field";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/lib/normalizer.ts
|
|
281
|
+
function normalizeFields(flat, rules = []) {
|
|
282
|
+
const sorted = [...rules].sort((a, b) => b.priority - a.priority);
|
|
283
|
+
return Object.entries(flat).map(
|
|
284
|
+
([key, value]) => normalizeField(key, value, sorted)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
function normalizeField(key, value, rules) {
|
|
288
|
+
const lookupKey = normaliseKey(key);
|
|
289
|
+
const builtIn = FIELD_MAP[lookupKey];
|
|
290
|
+
if (builtIn) {
|
|
291
|
+
return {
|
|
292
|
+
originalKey: key,
|
|
293
|
+
canonicalName: builtIn.canonicalName,
|
|
294
|
+
twentyType: builtIn.twentyType,
|
|
295
|
+
action: builtIn.action,
|
|
296
|
+
value,
|
|
297
|
+
source: "builtin"
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
for (const rule of rules) {
|
|
301
|
+
let matches = false;
|
|
302
|
+
try {
|
|
303
|
+
matches = rule.inputPattern === key || rule.inputPattern === lookupKey || new RegExp(rule.inputPattern, "i").test(key);
|
|
304
|
+
} catch {
|
|
305
|
+
matches = rule.inputPattern === key;
|
|
306
|
+
}
|
|
307
|
+
if (matches) {
|
|
308
|
+
return {
|
|
309
|
+
originalKey: key,
|
|
310
|
+
canonicalName: rule.canonicalName,
|
|
311
|
+
twentyType: rule.fieldType,
|
|
312
|
+
action: rule.action,
|
|
313
|
+
value,
|
|
314
|
+
source: "rule"
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const detectedType = detectType(value);
|
|
319
|
+
const classification = classify(key, value);
|
|
320
|
+
const canonicalName = classification === "field" ? toExtFieldName(key) : lookupKey;
|
|
321
|
+
return {
|
|
322
|
+
originalKey: key,
|
|
323
|
+
canonicalName,
|
|
324
|
+
twentyType: detectedType,
|
|
325
|
+
action: classification === "field" ? "field" : classification,
|
|
326
|
+
value,
|
|
327
|
+
source: "passthrough"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function toExtFieldName(key) {
|
|
331
|
+
const cleaned = key.replace(/[-_\s]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
332
|
+
return "ext" + cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
333
|
+
}
|
|
334
|
+
function partition(fields) {
|
|
335
|
+
const crmFields = [];
|
|
336
|
+
const noteFields = [];
|
|
337
|
+
const skipped = [];
|
|
338
|
+
for (const f of fields) {
|
|
339
|
+
if (f.action === "skip") skipped.push(f);
|
|
340
|
+
else if (f.action === "note") noteFields.push(f);
|
|
341
|
+
else crmFields.push(f);
|
|
342
|
+
}
|
|
343
|
+
return { crmFields, noteFields, skipped };
|
|
344
|
+
}
|
|
345
|
+
function assembleComposites(crmFields) {
|
|
346
|
+
const assembled = {};
|
|
347
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
348
|
+
const firstName = crmFields.find((f) => f.canonicalName === "firstName");
|
|
349
|
+
const lastName = crmFields.find((f) => f.canonicalName === "lastName");
|
|
350
|
+
const fullName = crmFields.find((f) => f.canonicalName === "fullName");
|
|
351
|
+
if (fullName && !firstName && !lastName) {
|
|
352
|
+
const parts = splitFullName(String(fullName.value ?? ""));
|
|
353
|
+
assembled["name"] = { firstName: parts[0], lastName: parts[1] };
|
|
354
|
+
consumed.add("fullName");
|
|
355
|
+
} else if (firstName || lastName) {
|
|
356
|
+
assembled["name"] = {
|
|
357
|
+
firstName: String(firstName?.value ?? ""),
|
|
358
|
+
lastName: String(lastName?.value ?? "")
|
|
359
|
+
};
|
|
360
|
+
if (firstName) consumed.add("firstName");
|
|
361
|
+
if (lastName) consumed.add("lastName");
|
|
362
|
+
}
|
|
363
|
+
const email = crmFields.find((f) => f.canonicalName === "email");
|
|
364
|
+
if (email) {
|
|
365
|
+
assembled["emails"] = { primaryEmail: String(email.value ?? "") };
|
|
366
|
+
consumed.add("email");
|
|
367
|
+
}
|
|
368
|
+
const phone = crmFields.find((f) => f.canonicalName === "phone");
|
|
369
|
+
if (phone) {
|
|
370
|
+
const raw = String(phone.value ?? "");
|
|
371
|
+
assembled["phones"] = {
|
|
372
|
+
primaryPhoneNumber: raw.replace(/\D/g, "").slice(-10),
|
|
373
|
+
primaryPhoneCountryCode: "US",
|
|
374
|
+
primaryPhoneCallingCode: "+1"
|
|
375
|
+
};
|
|
376
|
+
consumed.add("phone");
|
|
377
|
+
}
|
|
378
|
+
const domain = crmFields.find((f) => f.canonicalName === "domainName");
|
|
379
|
+
if (domain) {
|
|
380
|
+
assembled["domainName"] = {
|
|
381
|
+
primaryLinkUrl: String(domain.value ?? ""),
|
|
382
|
+
primaryLinkLabel: ""
|
|
383
|
+
};
|
|
384
|
+
consumed.add("domainName");
|
|
385
|
+
}
|
|
386
|
+
const addressFields = [
|
|
387
|
+
["addressStreet1", "addressStreet1"],
|
|
388
|
+
["addressStreet2", "addressStreet2"],
|
|
389
|
+
["addressCity", "addressCity"],
|
|
390
|
+
["addressState", "addressState"],
|
|
391
|
+
["addressPostcode", "addressPostcode"],
|
|
392
|
+
["addressCountry", "addressCountry"]
|
|
393
|
+
];
|
|
394
|
+
const addressParts = {};
|
|
395
|
+
for (const [canonical, outKey] of addressFields) {
|
|
396
|
+
const f = crmFields.find((x) => x.canonicalName === canonical);
|
|
397
|
+
if (f) {
|
|
398
|
+
addressParts[outKey] = f.value;
|
|
399
|
+
consumed.add(canonical);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (Object.keys(addressParts).length > 0) {
|
|
403
|
+
assembled["address"] = addressParts;
|
|
404
|
+
}
|
|
405
|
+
const remainder = crmFields.filter((f) => !consumed.has(f.canonicalName));
|
|
406
|
+
return { assembled, remainder };
|
|
407
|
+
}
|
|
408
|
+
function splitFullName(fullName) {
|
|
409
|
+
const parts = fullName.trim().split(/\s+/);
|
|
410
|
+
if (parts.length === 0) return ["", ""];
|
|
411
|
+
if (parts.length === 1) return [parts[0] ?? "", ""];
|
|
412
|
+
const lastName = parts.pop() ?? "";
|
|
413
|
+
return [parts.join(" "), lastName];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/lib/idempotency.ts
|
|
417
|
+
import { createHash } from "node:crypto";
|
|
418
|
+
function computePayloadHash(payload) {
|
|
419
|
+
const stable = stableStringify(payload);
|
|
420
|
+
return createHash("sha256").update(stable).digest("hex");
|
|
421
|
+
}
|
|
422
|
+
function stableStringify(value) {
|
|
423
|
+
if (value === null || value === void 0) return String(value);
|
|
424
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
425
|
+
if (Array.isArray(value)) {
|
|
426
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
427
|
+
}
|
|
428
|
+
const obj = value;
|
|
429
|
+
const sorted = Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",");
|
|
430
|
+
return "{" + sorted + "}";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/lib/note-formatter.ts
|
|
434
|
+
function formatNote(sourceName, fields) {
|
|
435
|
+
const lines = [];
|
|
436
|
+
lines.push(`**Source:** ${sourceName}`);
|
|
437
|
+
const entries = buildEntries(fields);
|
|
438
|
+
for (const { label, value } of entries) {
|
|
439
|
+
const formatted = formatValue(value);
|
|
440
|
+
if (formatted !== null) {
|
|
441
|
+
lines.push(`**${label}:** ${formatted}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return lines.join("\n");
|
|
445
|
+
}
|
|
446
|
+
function buildEntries(fields) {
|
|
447
|
+
return Object.entries(fields).map(([key, value]) => ({
|
|
448
|
+
label: toLabel(key),
|
|
449
|
+
value
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
function toLabel(key) {
|
|
453
|
+
return key.replace(/^ext_/, "").replace(/_/g, " ").replace(/([A-Z])/g, " $1").split(" ").map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(" ").trim();
|
|
454
|
+
}
|
|
455
|
+
function formatValue(value) {
|
|
456
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
457
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
458
|
+
if (typeof value === "object") {
|
|
459
|
+
try {
|
|
460
|
+
return JSON.stringify(value, null, 2);
|
|
461
|
+
} catch {
|
|
462
|
+
return String(value);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return String(value);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/lib/rest-client.ts
|
|
469
|
+
var apiUrl = () => process.env["TWENTY_API_URL"] ?? "";
|
|
470
|
+
var token = () => process.env["TWENTY_APP_ACCESS_TOKEN"] ?? "";
|
|
471
|
+
async function request(base, method, path, body) {
|
|
472
|
+
const res = await fetch(`${base}${path}`, {
|
|
473
|
+
method,
|
|
474
|
+
headers: {
|
|
475
|
+
Authorization: `Bearer ${token()}`,
|
|
476
|
+
"Content-Type": "application/json"
|
|
477
|
+
},
|
|
478
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
479
|
+
});
|
|
480
|
+
const text = await res.text();
|
|
481
|
+
if (!res.ok) {
|
|
482
|
+
throw new Error(`${method} ${path} \u2192 ${res.status}: ${text}`);
|
|
483
|
+
}
|
|
484
|
+
return JSON.parse(text);
|
|
485
|
+
}
|
|
486
|
+
var coreApi = {
|
|
487
|
+
get: (path) => request(`${apiUrl()}/rest`, "GET", path),
|
|
488
|
+
post: (path, body) => request(`${apiUrl()}/rest`, "POST", path, body),
|
|
489
|
+
patch: (path, body) => request(`${apiUrl()}/rest`, "PATCH", path, body)
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/logic-functions/test-ingest.ts
|
|
493
|
+
var handler = async (params) => {
|
|
494
|
+
const sourceSlug = params.pathParameters?.["slug"];
|
|
495
|
+
if (!sourceSlug) {
|
|
496
|
+
return { statusCode: 400, body: { error: "Missing source slug" } };
|
|
497
|
+
}
|
|
498
|
+
let rawPayload = {};
|
|
499
|
+
if (params.body && typeof params.body === "object" && !Array.isArray(params.body)) {
|
|
500
|
+
rawPayload = params.body;
|
|
501
|
+
}
|
|
502
|
+
const srcResult = await coreApi.get(
|
|
503
|
+
`/intakeSources?filter=slug[eq]:${encodeURIComponent(sourceSlug)}&first=1`
|
|
504
|
+
).catch(() => null);
|
|
505
|
+
const source = srcResult?.data?.intakeSources?.edges?.[0]?.node;
|
|
506
|
+
const structure = detectStructure(rawPayload);
|
|
507
|
+
const flat = structure.type === "flat" ? structure.data : flatten(structure.type === "structured" ? structure.extra ?? {} : rawPayload);
|
|
508
|
+
const normalized = normalizeFields(flat);
|
|
509
|
+
const { crmFields, noteFields, skipped } = partition(normalized);
|
|
510
|
+
const { assembled, remainder } = assembleComposites(crmFields);
|
|
511
|
+
for (const f of remainder) assembled[f.canonicalName] = f.value;
|
|
512
|
+
const extFields = Object.keys(assembled).filter((k) => /^ext[A-Z]/.test(k));
|
|
513
|
+
const standardFields = Object.keys(assembled).filter((k) => !/^ext[A-Z]/.test(k));
|
|
514
|
+
const noteOverflow = {};
|
|
515
|
+
for (const f of noteFields) noteOverflow[f.canonicalName] = f.value;
|
|
516
|
+
return {
|
|
517
|
+
statusCode: 200,
|
|
518
|
+
body: {
|
|
519
|
+
dryRun: true,
|
|
520
|
+
source: source ? { id: source.id, name: source.name } : null,
|
|
521
|
+
payloadHash: computePayloadHash(rawPayload),
|
|
522
|
+
payloadStructure: structure.type,
|
|
523
|
+
wouldCreate: {
|
|
524
|
+
standardFields,
|
|
525
|
+
customFieldsToCreate: extFields,
|
|
526
|
+
noteFields: noteFields.map((f) => f.canonicalName),
|
|
527
|
+
skipped: skipped.map((f) => f.originalKey)
|
|
528
|
+
},
|
|
529
|
+
notePreview: noteFields.length > 0 ? formatNote(source?.name ?? sourceSlug, noteOverflow) : null,
|
|
530
|
+
rawFlat: flat
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
var test_ingest_default = defineLogicFunction({
|
|
535
|
+
universalIdentifier: "3f7a9c21-8d4b-4e6f-a1c2-5b8d0f2e7a4c",
|
|
536
|
+
name: "intake-test",
|
|
537
|
+
description: "Dry-run: validates a payload and shows what would be created without writing to the CRM.",
|
|
538
|
+
timeoutSeconds: 15,
|
|
539
|
+
handler,
|
|
540
|
+
httpRouteTriggerSettings: {
|
|
541
|
+
path: "/intake/:slug/test",
|
|
542
|
+
httpMethod: "POST",
|
|
543
|
+
isAuthRequired: false
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
export {
|
|
547
|
+
test_ingest_default as default
|
|
548
|
+
};
|
|
549
|
+
//# sourceMappingURL=test-ingest.mjs.map
|