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.
@@ -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