twenty-app-intake 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -8
- package/manifest.json +419 -667
- package/package.json +6 -9
- package/public/cover.generated.png +0 -0
- package/src/front-components/dashboard.mjs +19 -21947
- package/src/front-components/dashboard.mjs.map +4 -4
- package/src/front-components/dashboard.tsx +118 -0
- package/src/front-components/settings-panel.mjs +19 -22103
- package/src/front-components/settings-panel.mjs.map +4 -4
- package/src/front-components/settings-panel.tsx +304 -0
- package/src/front-components/source-panel.mjs +20 -21962
- package/src/front-components/source-panel.mjs.map +4 -4
- package/src/front-components/source-panel.tsx +113 -0
- package/src/logic-functions/check-silence.mjs +263 -0
- package/src/logic-functions/check-silence.mjs.map +7 -0
- package/src/logic-functions/check-silence.ts +52 -0
- package/src/logic-functions/contract.mjs +507 -0
- package/src/logic-functions/contract.mjs.map +7 -0
- package/src/logic-functions/contract.ts +172 -0
- package/src/logic-functions/health.mjs +125 -5
- package/src/logic-functions/health.mjs.map +3 -3
- package/src/logic-functions/health.ts +62 -0
- package/src/logic-functions/quarantine-discard.mjs +380 -0
- package/src/logic-functions/quarantine-discard.mjs.map +7 -0
- package/src/logic-functions/quarantine-discard.ts +65 -0
- package/src/logic-functions/quarantine-list.mjs +211 -0
- package/src/logic-functions/quarantine-list.mjs.map +7 -0
- package/src/logic-functions/quarantine-list.ts +93 -0
- package/src/logic-functions/quarantine-release.mjs +1974 -0
- package/src/logic-functions/quarantine-release.mjs.map +7 -0
- package/src/logic-functions/quarantine-release.ts +94 -0
- package/src/logic-functions/register.mjs +34 -1
- package/src/logic-functions/register.mjs.map +2 -2
- package/src/logic-functions/register.ts +105 -0
- package/src/logic-functions/replay-bulk.mjs +2040 -0
- package/src/logic-functions/replay-bulk.mjs.map +7 -0
- package/src/logic-functions/replay-bulk.ts +83 -0
- package/src/logic-functions/replay-log.mjs +1987 -0
- package/src/logic-functions/replay-log.mjs.map +7 -0
- package/src/logic-functions/replay-log.ts +54 -0
- package/src/logic-functions/retry.mjs +1004 -248
- package/src/logic-functions/retry.mjs.map +4 -4
- package/src/logic-functions/retry.ts +101 -0
- package/src/logic-functions/test-ingest.mjs +616 -28
- package/src/logic-functions/test-ingest.mjs.map +4 -4
- package/src/logic-functions/test-ingest.ts +179 -0
- package/src/logic-functions/webhook.mjs +968 -209
- package/src/logic-functions/webhook.mjs.map +4 -4
- package/src/logic-functions/webhook.ts +58 -0
- package/src/post-install.mjs +34 -1
- package/src/post-install.mjs.map +2 -2
- package/src/post-install.ts +81 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
|
2
|
+
import { CoreApiClient } from 'twenty-client-sdk/core';
|
|
3
|
+
import { detectStructure, flatten } from '../lib/flattener';
|
|
4
|
+
import { normalizeFields, partition, type DatabaseRule } from '../lib/normalizer';
|
|
5
|
+
import { computePayloadHash } from '../lib/idempotency';
|
|
6
|
+
import { formatNote } from '../lib/note-formatter';
|
|
7
|
+
import { coreApi } from '../lib/rest-client';
|
|
8
|
+
import { buildDiff } from '../lib/diff';
|
|
9
|
+
import { scoreSpam, describeVerdict } from '../lib/spam';
|
|
10
|
+
import { honeypotField, mergePolicy, spamFilterEnabled, spamScoreThreshold } from '../lib/settings';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Dry run: what would happen, without it happening.
|
|
14
|
+
*
|
|
15
|
+
* Two things changed in 0.4.0 and both were correctness, not presentation. The
|
|
16
|
+
* preview now loads the same field rules the live pipeline loads — it previously
|
|
17
|
+
* ran with none, so any payload relying on a rule was previewed wrongly. And the
|
|
18
|
+
* result is a structured diff rather than a list of names, so it can be asserted
|
|
19
|
+
* against in a test or read by a deployment check instead of eyeballed.
|
|
20
|
+
*/
|
|
21
|
+
const handler = async (params: RoutePayload) => {
|
|
22
|
+
const sourceSlug = params.pathParameters?.['slug'];
|
|
23
|
+
if (!sourceSlug) return { statusCode: 400, body: { error: 'Missing source slug' } };
|
|
24
|
+
|
|
25
|
+
let rawPayload: Record<string, unknown> = {};
|
|
26
|
+
if (params.body && typeof params.body === 'object' && !Array.isArray(params.body)) {
|
|
27
|
+
rawPayload = params.body as Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const source = await loadSource(sourceSlug);
|
|
31
|
+
const rules = await loadRules(source?.id);
|
|
32
|
+
|
|
33
|
+
const structure = detectStructure(rawPayload);
|
|
34
|
+
const flat = structure.type === 'flat'
|
|
35
|
+
? structure.data
|
|
36
|
+
: flatten(structure.extra ?? {});
|
|
37
|
+
|
|
38
|
+
const normalized = normalizeFields(flat, rules);
|
|
39
|
+
const { crmFields, noteFields, skipped } = partition(normalized);
|
|
40
|
+
|
|
41
|
+
const honeypot = source?.honeypotField || honeypotField();
|
|
42
|
+
const verdict = scoreSpam(flat, honeypot);
|
|
43
|
+
const wouldQuarantine = verdict.honeypot
|
|
44
|
+
|| (spamFilterEnabled() && verdict.score >= spamScoreThreshold());
|
|
45
|
+
|
|
46
|
+
const warnings: string[] = [];
|
|
47
|
+
if (!source) warnings.push(`No source registered with slug "${sourceSlug}" — previewing with default settings. A live request would return 404.`);
|
|
48
|
+
if (source?.status === 'PAUSED') warnings.push('This source is PAUSED — a live request would return 423.');
|
|
49
|
+
|
|
50
|
+
const noteOverflow: Record<string, unknown> = {};
|
|
51
|
+
for (const f of noteFields) noteOverflow[f.canonicalName] = f.value;
|
|
52
|
+
|
|
53
|
+
// A payload that would be held never reaches the record stages, so describing
|
|
54
|
+
// what it would write would be describing something that will not happen.
|
|
55
|
+
const diff = wouldQuarantine ? null : await buildDiff({
|
|
56
|
+
crmFields,
|
|
57
|
+
noteFields,
|
|
58
|
+
skipped,
|
|
59
|
+
structuredCompany: structure.type === 'structured' ? (structure.company ?? undefined) : undefined,
|
|
60
|
+
structuredPerson: structure.type === 'structured' ? (structure.person ?? undefined) : undefined,
|
|
61
|
+
createOpportunity: source?.createOpportunity ?? true,
|
|
62
|
+
targetObject: (source?.targetObject as 'PERSON' | 'COMPANY' | 'AUTO') ?? 'AUTO',
|
|
63
|
+
policy: parsePolicy(source?.mergePolicy) ?? mergePolicy(),
|
|
64
|
+
warnings,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
statusCode: 200,
|
|
69
|
+
body: {
|
|
70
|
+
dryRun: true,
|
|
71
|
+
source: source ? { id: source.id, name: source.name, slug: source.slug, status: source.status } : null,
|
|
72
|
+
payloadHash: computePayloadHash(rawPayload),
|
|
73
|
+
payloadStructure: structure.type,
|
|
74
|
+
|
|
75
|
+
spam: {
|
|
76
|
+
score: verdict.score,
|
|
77
|
+
threshold: spamScoreThreshold(),
|
|
78
|
+
filterEnabled: spamFilterEnabled(),
|
|
79
|
+
honeypotField: honeypot || null,
|
|
80
|
+
wouldQuarantine,
|
|
81
|
+
signals: verdict.signals,
|
|
82
|
+
reason: verdict.signals.length > 0 ? describeVerdict(verdict) : null,
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
mergePolicy: parsePolicy(source?.mergePolicy) ?? mergePolicy(),
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Per object: whether it would be created or updated, which record it
|
|
89
|
+
* matched, what each field would do to the value already there, and which
|
|
90
|
+
* fields do not exist yet and would be added to the schema.
|
|
91
|
+
*/
|
|
92
|
+
diff,
|
|
93
|
+
|
|
94
|
+
note: noteFields.length > 0
|
|
95
|
+
? { fields: noteFields.map((f) => f.canonicalName), preview: formatNote(source?.name ?? sourceSlug, noteOverflow) }
|
|
96
|
+
: null,
|
|
97
|
+
|
|
98
|
+
rulesApplied: rules.length,
|
|
99
|
+
warnings,
|
|
100
|
+
rawFlat: flat,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type SourceNode = {
|
|
106
|
+
id: string;
|
|
107
|
+
name: string;
|
|
108
|
+
slug: string;
|
|
109
|
+
status: string;
|
|
110
|
+
targetObject: string;
|
|
111
|
+
createOpportunity: boolean | null;
|
|
112
|
+
honeypotField?: string | null;
|
|
113
|
+
mergePolicy?: string | null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
async function loadSource(slug: string): Promise<SourceNode | null> {
|
|
117
|
+
try {
|
|
118
|
+
const res = await coreApi.get<{ data: { intakeSources: { edges: Array<{ node: SourceNode }> } } }>(
|
|
119
|
+
`/intakeSources?filter=slug[eq]:${encodeURIComponent(slug)}&first=1`,
|
|
120
|
+
);
|
|
121
|
+
return res.data?.intakeSources?.edges?.[0]?.node ?? null;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The same query NormalizeStage runs, so the preview sees the same rules. */
|
|
128
|
+
async function loadRules(sourceId: string | undefined): Promise<DatabaseRule[]> {
|
|
129
|
+
try {
|
|
130
|
+
const client = new CoreApiClient();
|
|
131
|
+
const filter = sourceId
|
|
132
|
+
? { or: [{ intakeSource: { id: { eq: sourceId } } }, { intakeSource: { id: { is: 'NULL' } } }] }
|
|
133
|
+
: { intakeSource: { id: { is: 'NULL' } } };
|
|
134
|
+
|
|
135
|
+
const result = await client.query({
|
|
136
|
+
intakeFieldRules: {
|
|
137
|
+
__args: {
|
|
138
|
+
filter: { and: [filter, { isActive: { eq: true } }] },
|
|
139
|
+
orderBy: { priority: 'DescNullsLast' },
|
|
140
|
+
},
|
|
141
|
+
edges: {
|
|
142
|
+
node: {
|
|
143
|
+
inputPattern: true,
|
|
144
|
+
canonicalName: true,
|
|
145
|
+
fieldType: true,
|
|
146
|
+
priority: true,
|
|
147
|
+
targetObject: true,
|
|
148
|
+
mergeStrategy: true,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return (result.intakeFieldRules?.edges ?? []).map((e: { node: DatabaseRule }) => e.node);
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parsePolicy(raw: string | null | undefined): 'PRESERVE' | 'NEWEST_WINS' | null {
|
|
161
|
+
switch ((raw ?? '').toUpperCase()) {
|
|
162
|
+
case 'PRESERVE': return 'PRESERVE';
|
|
163
|
+
case 'NEWEST_WINS': return 'NEWEST_WINS';
|
|
164
|
+
default: return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export default defineLogicFunction({
|
|
169
|
+
universalIdentifier: '3f7a9c21-8d4b-4e6f-a1c2-5b8d0f2e7a4c',
|
|
170
|
+
name: 'intake-test',
|
|
171
|
+
description: 'Dry-run: returns a structured diff of what would be created or updated, writing nothing.',
|
|
172
|
+
timeoutSeconds: 20,
|
|
173
|
+
handler,
|
|
174
|
+
httpRouteTriggerSettings: {
|
|
175
|
+
path: '/intake/:slug/test',
|
|
176
|
+
httpMethod: 'POST',
|
|
177
|
+
isAuthRequired: false,
|
|
178
|
+
},
|
|
179
|
+
});
|