@reachy/audience-module 1.0.18 → 1.0.19

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,803 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClickHouseEventQueryExecutor = void 0;
4
+ const MAX_FETCH_ROWS = 200000;
5
+ function safeStr(v) {
6
+ return v === null || v === undefined ? '' : String(v).trim();
7
+ }
8
+ function resolveContactId(row, reachyIdToContactId, allContactIds) {
9
+ const cid = safeStr(row.contact_id);
10
+ if (cid && allContactIds.has(cid))
11
+ return cid;
12
+ const rid = safeStr(row.reachy_id);
13
+ if (rid) {
14
+ const mapped = reachyIdToContactId.get(rid);
15
+ if (mapped)
16
+ return mapped;
17
+ }
18
+ return null;
19
+ }
20
+ function chDateTime(iso) {
21
+ return iso.replace('T', ' ').replace('Z', '').split('.')[0] ?? iso.replace('T', ' ').replace('Z', '');
22
+ }
23
+ const COLUMN_FIELDS = new Set([
24
+ 'current_url', 'domain', 'path', 'referrer', 'page_title',
25
+ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
26
+ 'session_id', 'session_is_new',
27
+ ]);
28
+ const SAFE_FIELD_RE = /^[a-zA-Z0-9_]+$/;
29
+ function isSafeFieldName(name) {
30
+ return SAFE_FIELD_RE.test(name);
31
+ }
32
+ function resolveEventFieldExpr(keyRaw) {
33
+ const key = safeStr(keyRaw);
34
+ if (!key)
35
+ return { expr: "''", kind: 'string' };
36
+ if (COLUMN_FIELDS.has(key))
37
+ return { expr: key, kind: 'string' };
38
+ const extractStr = (base, path) => {
39
+ const parts = path.split('.').filter(Boolean);
40
+ if (!parts.every(isSafeFieldName))
41
+ return "''";
42
+ if (parts.length === 0)
43
+ return `JSONExtractString(${base}, 'value')`;
44
+ if (parts.length === 1)
45
+ return `JSONExtractString(${base}, '${parts[0]}')`;
46
+ let expr = base;
47
+ for (let i = 0; i < parts.length - 1; i++) {
48
+ expr = `JSONExtractRaw(${expr}, '${parts[i]}')`;
49
+ }
50
+ return `JSONExtractString(${expr}, '${parts[parts.length - 1]}')`;
51
+ };
52
+ if (key.startsWith('event_data.'))
53
+ return { expr: extractStr('event_data', key.replace(/^event_data\./, '')), kind: 'string' };
54
+ if (key.startsWith('custom_data.'))
55
+ return { expr: extractStr("JSONExtractRaw(event_data, 'custom_data')", key.replace(/^custom_data\./, '')), kind: 'string' };
56
+ if (key.startsWith('url_data.'))
57
+ return { expr: extractStr("JSONExtractRaw(event_data, 'url_data')", key.replace(/^url_data\./, '')), kind: 'string' };
58
+ if (key.startsWith('utm_data.'))
59
+ return { expr: extractStr("JSONExtractRaw(event_data, 'utm_data')", key.replace(/^utm_data\./, '')), kind: 'string' };
60
+ if (key.startsWith('session_data.'))
61
+ return { expr: extractStr("JSONExtractRaw(event_data, 'session_data')", key.replace(/^session_data\./, '')), kind: 'string' };
62
+ if (key.includes('.'))
63
+ return { expr: extractStr("JSONExtractRaw(event_data, 'custom_data')", key), kind: 'string' };
64
+ if (!isSafeFieldName(key))
65
+ return { expr: "''", kind: 'string' };
66
+ return { expr: `JSONExtractString(event_data, '${key}')`, kind: 'string' };
67
+ }
68
+ function buildAttributeWhereClause(attributes, params, paramIdx) {
69
+ if (!Array.isArray(attributes) || attributes.length === 0)
70
+ return '';
71
+ const parts = [];
72
+ for (const attr of attributes) {
73
+ const key = safeStr(attr.key);
74
+ if (!key)
75
+ continue;
76
+ const op = safeStr(attr.op) || 'contains';
77
+ const value = attr.value;
78
+ if (value === null || value === undefined)
79
+ continue;
80
+ const { expr } = resolveEventFieldExpr(key);
81
+ if (expr === "''")
82
+ continue;
83
+ const pName = `attr_v_${paramIdx.n++}`;
84
+ const strValue = String(value);
85
+ switch (op) {
86
+ case 'equals':
87
+ params[pName] = strValue;
88
+ parts.push(`${expr} = {${pName}:String}`);
89
+ break;
90
+ case 'not_equals':
91
+ params[pName] = strValue;
92
+ parts.push(`${expr} != {${pName}:String}`);
93
+ break;
94
+ case 'starts_with':
95
+ params[pName] = `${strValue}%`;
96
+ parts.push(`${expr} LIKE {${pName}:String}`);
97
+ break;
98
+ case 'ends_with':
99
+ params[pName] = `%${strValue}`;
100
+ parts.push(`${expr} LIKE {${pName}:String}`);
101
+ break;
102
+ case 'contains':
103
+ default:
104
+ params[pName] = `%${strValue}%`;
105
+ parts.push(`${expr} LIKE {${pName}:String}`);
106
+ break;
107
+ }
108
+ }
109
+ return parts.length > 0 ? parts.join(' AND ') : '';
110
+ }
111
+ function buildTimeWhereClause(rule, cfg, params, paramIdx) {
112
+ const parts = [];
113
+ if (rule.time) {
114
+ if (rule.time.unit && rule.time.value != null) {
115
+ const unit = String(rule.time.unit);
116
+ const value = Number(rule.time.value);
117
+ const unitMs = {
118
+ minutes: 60000, hours: 3600000, days: 86400000, weeks: 604800000, months: 2592000000
119
+ };
120
+ const from = new Date(Date.now() - value * (unitMs[unit] ?? unitMs.days));
121
+ const pName = `time_from_${paramIdx.n++}`;
122
+ params[pName] = chDateTime(from.toISOString());
123
+ parts.push(`event_timestamp >= {${pName}:DateTime64(3)}`);
124
+ }
125
+ else {
126
+ if (rule.time.from) {
127
+ const pName = `time_from_${paramIdx.n++}`;
128
+ params[pName] = chDateTime(String(rule.time.from));
129
+ parts.push(`event_timestamp >= {${pName}:DateTime64(3)}`);
130
+ }
131
+ if (rule.time.to) {
132
+ const pName = `time_to_${paramIdx.n++}`;
133
+ params[pName] = chDateTime(String(rule.time.to));
134
+ parts.push(`event_timestamp <= {${pName}:DateTime64(3)}`);
135
+ }
136
+ }
137
+ }
138
+ else if (cfg && cfg.timeFrame) {
139
+ const tf = String(cfg.timeFrame).trim();
140
+ let unit = 'days';
141
+ let value = 7;
142
+ if (/^\d+$/.test(tf)) {
143
+ value = Number(tf);
144
+ unit = 'days';
145
+ }
146
+ else if (/^\d+\s*m$/.test(tf)) {
147
+ value = Number(tf.replace(/m$/, ''));
148
+ unit = 'minutes';
149
+ }
150
+ else if (/^\d+\s*h$/.test(tf)) {
151
+ value = Number(tf.replace(/h$/, ''));
152
+ unit = 'hours';
153
+ }
154
+ else if (/^\d+\s*d$/.test(tf)) {
155
+ value = Number(tf.replace(/d$/, ''));
156
+ unit = 'days';
157
+ }
158
+ else if (/^\d+\s*w$/.test(tf)) {
159
+ value = Number(tf.replace(/w$/, ''));
160
+ unit = 'weeks';
161
+ }
162
+ else if (/^\d+\s*mo$/.test(tf)) {
163
+ value = Number(tf.replace(/mo$/, ''));
164
+ unit = 'months';
165
+ }
166
+ const unitMs = {
167
+ minutes: 60000, hours: 3600000, days: 86400000, weeks: 604800000, months: 2592000000
168
+ };
169
+ const from = new Date(Date.now() - value * (unitMs[unit] ?? unitMs.days));
170
+ const pName = `time_from_${paramIdx.n++}`;
171
+ params[pName] = chDateTime(from.toISOString());
172
+ parts.push(`event_timestamp >= {${pName}:DateTime64(3)}`);
173
+ }
174
+ return parts.join(' AND ');
175
+ }
176
+ class ClickHouseEventQueryExecutor {
177
+ constructor(client, logger = console) {
178
+ this.client = client;
179
+ this.logger = logger;
180
+ }
181
+ async execute(rule, criteria, organizationId, projectId, projectTimezone, reachyIdToContactId, _contactIdToReachyId, allContactIds) {
182
+ try {
183
+ const cfg = criteria?.config || {};
184
+ const typeId = String(criteria?.type || '');
185
+ const effectiveEventName = cfg.eventType && String(cfg.eventType).trim() !== ''
186
+ ? String(cfg.eventType)
187
+ : String(rule.eventName);
188
+ const ruleFiltersAll = Array.isArray(rule.filters) ? rule.filters : [];
189
+ const hasFirstTime = ruleFiltersAll.some((f) => String(f?.type || '').trim() === 'first_time');
190
+ const hasLastTime = ruleFiltersAll.some((f) => String(f?.type || '').trim() === 'last_time');
191
+ const ruleFilters = ruleFiltersAll.filter((f) => {
192
+ const t = String(f?.type || '').trim();
193
+ return t && t !== 'first_time' && t !== 'last_time';
194
+ });
195
+ const needsInMemory = ruleFilters.some((f) => ['time_of_day', 'day_of_week', 'day_of_month', 'event_property'].includes(String(f?.type || '').trim())) ||
196
+ !!(rule.interest && rule.interest.key);
197
+ const hasPureFrequency = rule.frequency && rule.frequency.value != null;
198
+ const hasFirstOrLast = hasFirstTime || hasLastTime;
199
+ if (hasFirstOrLast && !hasFirstTime && hasLastTime) {
200
+ return this._executeLastTime(rule, cfg, organizationId, projectId, effectiveEventName, reachyIdToContactId, allContactIds);
201
+ }
202
+ if (hasFirstTime && !hasLastTime) {
203
+ return this._executeFirstTime(rule, cfg, organizationId, projectId, effectiveEventName, reachyIdToContactId, allContactIds);
204
+ }
205
+ if (hasFirstTime && hasLastTime) {
206
+ return this._executeFirstAndLastTime(rule, cfg, organizationId, projectId, effectiveEventName, reachyIdToContactId, allContactIds);
207
+ }
208
+ if (hasPureFrequency && !needsInMemory) {
209
+ return this._executeFrequency(rule, cfg, organizationId, projectId, effectiveEventName, typeId, reachyIdToContactId, allContactIds);
210
+ }
211
+ return this._executeFetchAndProcess(rule, cfg, organizationId, projectId, effectiveEventName, typeId, projectTimezone, ruleFilters, reachyIdToContactId, allContactIds);
212
+ }
213
+ catch (err) {
214
+ this.logger.error('[ClickHouseEventQueryExecutor] execute error:', err);
215
+ return new Set();
216
+ }
217
+ }
218
+ async _executeFirstTime(rule, cfg, orgId, projId, eventName, reachyIdToContactId, allContactIds) {
219
+ const params = {};
220
+ const paramIdx = { n: 0 };
221
+ const windowBounds = this._resolveWindowBounds(rule, cfg);
222
+ let whereClause = this._baseWhere(orgId, projId, eventName, params);
223
+ const attrWhere = this._buildAttributeWhere(rule, params, paramIdx);
224
+ if (attrWhere)
225
+ whereClause += ` AND ${attrWhere}`;
226
+ let havingClause = '';
227
+ if (windowBounds.start) {
228
+ params['ft_window_start'] = chDateTime(windowBounds.start);
229
+ havingClause = `min(event_timestamp) >= {ft_window_start:DateTime64(3)}`;
230
+ }
231
+ if (windowBounds.end) {
232
+ params['ft_window_end'] = chDateTime(windowBounds.end);
233
+ whereClause += ` AND event_timestamp <= {ft_window_end:DateTime64(3)}`;
234
+ }
235
+ const query = `
236
+ SELECT contact_id, reachy_id
237
+ FROM contact_events
238
+ WHERE ${whereClause}
239
+ GROUP BY contact_id, reachy_id
240
+ ${havingClause ? `HAVING ${havingClause}` : ''}
241
+ `;
242
+ return this._queryToContactIdSet(query, params, reachyIdToContactId, allContactIds);
243
+ }
244
+ async _executeLastTime(rule, cfg, orgId, projId, eventName, reachyIdToContactId, allContactIds) {
245
+ const params = {};
246
+ const paramIdx = { n: 0 };
247
+ const windowBounds = this._resolveWindowBounds(rule, cfg);
248
+ let whereClause = this._baseWhere(orgId, projId, eventName, params);
249
+ const attrWhere = this._buildAttributeWhere(rule, params, paramIdx);
250
+ if (attrWhere)
251
+ whereClause += ` AND ${attrWhere}`;
252
+ if (windowBounds.start) {
253
+ params['lt_window_start'] = chDateTime(windowBounds.start);
254
+ whereClause += ` AND event_timestamp >= {lt_window_start:DateTime64(3)}`;
255
+ }
256
+ let havingClause = '';
257
+ if (windowBounds.end) {
258
+ params['lt_window_end'] = chDateTime(windowBounds.end);
259
+ havingClause = `max(event_timestamp) <= {lt_window_end:DateTime64(3)}`;
260
+ }
261
+ const query = `
262
+ SELECT contact_id, reachy_id
263
+ FROM contact_events
264
+ WHERE ${whereClause}
265
+ GROUP BY contact_id, reachy_id
266
+ ${havingClause ? `HAVING ${havingClause}` : ''}
267
+ `;
268
+ return this._queryToContactIdSet(query, params, reachyIdToContactId, allContactIds);
269
+ }
270
+ async _executeFirstAndLastTime(rule, cfg, orgId, projId, eventName, reachyIdToContactId, allContactIds) {
271
+ const params = {};
272
+ const windowBounds = this._resolveWindowBounds(rule, cfg);
273
+ const whereClause = this._baseWhere(orgId, projId, eventName, params);
274
+ const havingParts = [];
275
+ if (windowBounds.start) {
276
+ params['flt_window_start'] = chDateTime(windowBounds.start);
277
+ havingParts.push(`min(event_timestamp) >= {flt_window_start:DateTime64(3)}`);
278
+ }
279
+ if (windowBounds.end) {
280
+ params['flt_window_end'] = chDateTime(windowBounds.end);
281
+ havingParts.push(`max(event_timestamp) <= {flt_window_end:DateTime64(3)}`);
282
+ }
283
+ const query = `
284
+ SELECT contact_id, reachy_id
285
+ FROM contact_events
286
+ WHERE ${whereClause}
287
+ GROUP BY contact_id, reachy_id
288
+ ${havingParts.length > 0 ? `HAVING ${havingParts.join(' AND ')}` : ''}
289
+ `;
290
+ return this._queryToContactIdSet(query, params, reachyIdToContactId, allContactIds);
291
+ }
292
+ async _executeFrequency(rule, cfg, orgId, projId, eventName, typeId, reachyIdToContactId, allContactIds) {
293
+ const params = {};
294
+ const paramIdx = { n: 0 };
295
+ const { op, value, value2, type: freqType = 'count', field = 'value' } = rule.frequency;
296
+ let whereClause = this._baseWhere(orgId, projId, eventName, params);
297
+ const timeWhere = buildTimeWhereClause(rule, cfg, params, paramIdx);
298
+ if (timeWhere)
299
+ whereClause += ` AND ${timeWhere}`;
300
+ const attrWhere = this._buildAttributeWhere(rule, params, paramIdx);
301
+ if (attrWhere)
302
+ whereClause += ` AND ${attrWhere}`;
303
+ const liveWhere = this._buildLivePresetWhere(cfg, typeId, params, paramIdx);
304
+ if (liveWhere)
305
+ whereClause += ` AND ${liveWhere}`;
306
+ let aggExpr = 'count()';
307
+ if (freqType === 'sum') {
308
+ const { expr } = resolveEventFieldExpr(field);
309
+ aggExpr = `sum(toFloat64OrZero(${expr}))`;
310
+ }
311
+ else if (freqType === 'avg') {
312
+ const { expr } = resolveEventFieldExpr(field);
313
+ aggExpr = `avg(toFloat64OrZero(${expr}))`;
314
+ }
315
+ params['freq_val'] = Number(value);
316
+ let having = '';
317
+ if (op === 'between') {
318
+ params['freq_val2'] = Number(value2);
319
+ having = `agg_val >= {freq_val:Float64} AND agg_val <= {freq_val2:Float64}`;
320
+ }
321
+ else {
322
+ having = `agg_val ${op} {freq_val:Float64}`;
323
+ }
324
+ const cleanQuery = `
325
+ SELECT contact_id, reachy_id
326
+ FROM (
327
+ SELECT contact_id, reachy_id, ${aggExpr} AS agg_val
328
+ FROM contact_events
329
+ WHERE ${whereClause}
330
+ GROUP BY contact_id, reachy_id
331
+ )
332
+ WHERE ${having}
333
+ `;
334
+ return this._queryToContactIdSet(cleanQuery, params, reachyIdToContactId, allContactIds);
335
+ }
336
+ async _executeFetchAndProcess(rule, cfg, orgId, projId, eventName, typeId, projectTimezone, ruleFilters, reachyIdToContactId, allContactIds) {
337
+ const params = {};
338
+ const paramIdx = { n: 0 };
339
+ let whereClause = this._baseWhere(orgId, projId, eventName, params);
340
+ const timeWhere = buildTimeWhereClause(rule, cfg, params, paramIdx);
341
+ if (timeWhere)
342
+ whereClause += ` AND ${timeWhere}`;
343
+ const attrWhere = this._buildAttributeWhere(rule, params, paramIdx);
344
+ if (attrWhere)
345
+ whereClause += ` AND ${attrWhere}`;
346
+ const liveWhere = this._buildLivePresetWhere(cfg, typeId, params, paramIdx);
347
+ if (liveWhere)
348
+ whereClause += ` AND ${liveWhere}`;
349
+ const needsEventData = !!((rule.interest && rule.interest.key) ||
350
+ ruleFilters.some((f) => ['event_property'].includes(String(f?.type || '').trim())) ||
351
+ (rule.frequency && rule.frequency.type && rule.frequency.type !== 'count') ||
352
+ typeId === 'live-page-count');
353
+ const needsTimestamp = ruleFilters.some((f) => ['time_of_day', 'day_of_week', 'day_of_month'].includes(String(f?.type || '').trim()));
354
+ let selectFields = 'contact_id, reachy_id';
355
+ if (needsEventData)
356
+ selectFields += ', event_data';
357
+ if (needsTimestamp)
358
+ selectFields += ', event_timestamp';
359
+ if (needsEventData || needsTimestamp)
360
+ selectFields += ', event_name';
361
+ const query = `
362
+ SELECT ${selectFields}
363
+ FROM contact_events
364
+ WHERE ${whereClause}
365
+ LIMIT ${MAX_FETCH_ROWS}
366
+ `;
367
+ let rows = [];
368
+ try {
369
+ const result = await this.client.query({ query, query_params: params, format: 'JSONEachRow' });
370
+ rows = await result.json();
371
+ }
372
+ catch (err) {
373
+ this.logger.error('[ClickHouseEventQueryExecutor] fetch error:', err);
374
+ return new Set();
375
+ }
376
+ if (!rows || rows.length === 0)
377
+ return new Set();
378
+ if (needsEventData) {
379
+ for (const r of rows) {
380
+ if (typeof r.event_data === 'string') {
381
+ try {
382
+ r.event_data = JSON.parse(r.event_data);
383
+ }
384
+ catch {
385
+ r.event_data = {};
386
+ }
387
+ }
388
+ }
389
+ }
390
+ if (ruleFilters.length > 0) {
391
+ rows = applyEventRuleFilters(rows, ruleFilters, { timezone: projectTimezone });
392
+ }
393
+ const interest = rule.interest;
394
+ if (interest && interest.key) {
395
+ return computeInterestContactIds(rows, interest, (row) => {
396
+ return resolveContactId(row, reachyIdToContactId, allContactIds);
397
+ });
398
+ }
399
+ if (typeId === 'live-page-count' && cfg && cfg.pageCount != null) {
400
+ const threshold = Number(cfg.pageCount);
401
+ const counts = new Map();
402
+ for (const row of rows) {
403
+ const cid = resolveContactId(row, reachyIdToContactId, allContactIds);
404
+ if (cid)
405
+ counts.set(cid, (counts.get(cid) || 0) + 1);
406
+ }
407
+ const res = new Set();
408
+ counts.forEach((cnt, id) => { if (cnt >= threshold)
409
+ res.add(id); });
410
+ return res;
411
+ }
412
+ if (rule.frequency && rule.frequency.value != null) {
413
+ const { op, value, value2, type: freqType = 'count', field = 'value' } = rule.frequency;
414
+ const counts = new Map();
415
+ const denoms = new Map();
416
+ for (const row of rows) {
417
+ const cid = resolveContactId(row, reachyIdToContactId, allContactIds);
418
+ if (!cid)
419
+ continue;
420
+ if (freqType === 'count') {
421
+ counts.set(cid, (counts.get(cid) || 0) + 1);
422
+ }
423
+ else {
424
+ const raw = getEventPropertyValue(row.event_data, field) ?? row.event_data?.value ?? row.event_data?.amount;
425
+ const numVal = Number(raw);
426
+ if (Number.isFinite(numVal)) {
427
+ counts.set(cid, (counts.get(cid) || 0) + numVal);
428
+ if (freqType === 'avg')
429
+ denoms.set(cid, (denoms.get(cid) || 0) + 1);
430
+ }
431
+ }
432
+ }
433
+ const res = new Set();
434
+ counts.forEach((agg, id) => {
435
+ let finalVal = agg;
436
+ if (freqType === 'avg') {
437
+ const d = denoms.get(id) || 0;
438
+ finalVal = d > 0 ? agg / d : 0;
439
+ }
440
+ if (op === 'between') {
441
+ if (finalVal >= Number(value) && finalVal <= Number(value2))
442
+ res.add(id);
443
+ }
444
+ else if ((op === '>=' && finalVal >= value) || (op === '>' && finalVal > value) ||
445
+ (op === '=' && finalVal === value) || (op === '!=' && finalVal !== value) ||
446
+ (op === '<=' && finalVal <= value) || (op === '<' && finalVal < value)) {
447
+ res.add(id);
448
+ }
449
+ });
450
+ return res;
451
+ }
452
+ const res = new Set();
453
+ for (const row of rows) {
454
+ const cid = resolveContactId(row, reachyIdToContactId, allContactIds);
455
+ if (cid)
456
+ res.add(cid);
457
+ }
458
+ return res;
459
+ }
460
+ _baseWhere(orgId, projId, eventName, params) {
461
+ params['p_org_id'] = orgId;
462
+ params['p_proj_id'] = projId;
463
+ params['p_event_name'] = eventName;
464
+ return `organization_id = {p_org_id:String} AND project_id = {p_proj_id:String} AND event_name = {p_event_name:String}`;
465
+ }
466
+ _buildAttributeWhere(rule, params, paramIdx) {
467
+ let attributes = Array.isArray(rule.attributes) ? rule.attributes : (rule.attributes ? [rule.attributes] : []);
468
+ const interestKey = rule.interest?.key ? String(rule.interest.key).trim() : '';
469
+ if (interestKey) {
470
+ attributes = attributes.filter((a) => String(a?.key || '').trim() !== interestKey);
471
+ }
472
+ return buildAttributeWhereClause(attributes, params, paramIdx);
473
+ }
474
+ _buildLivePresetWhere(cfg, typeId, params, paramIdx) {
475
+ const parts = [];
476
+ if (typeId === 'live-page-visit') {
477
+ const v = cfg.pageUrl;
478
+ const d = cfg.domain;
479
+ const ors = [];
480
+ if (v && String(v).trim()) {
481
+ const pPath = `lp_path_${paramIdx.n++}`;
482
+ const pUrl = `lp_url_${paramIdx.n++}`;
483
+ params[pPath] = `%${String(v)}%`;
484
+ params[pUrl] = `%${String(v)}%`;
485
+ ors.push(`path LIKE {${pPath}:String}`);
486
+ ors.push(`current_url LIKE {${pUrl}:String}`);
487
+ }
488
+ if (d && String(d).trim()) {
489
+ const pDomain = `lp_domain_${paramIdx.n++}`;
490
+ params[pDomain] = `%${String(d)}%`;
491
+ ors.push(`domain LIKE {${pDomain}:String}`);
492
+ }
493
+ if (ors.length > 0)
494
+ parts.push(`(${ors.join(' OR ')})`);
495
+ }
496
+ if (typeId === 'live-referrer') {
497
+ const v = cfg.referrerUrl;
498
+ if (v && String(v).trim()) {
499
+ const pRef = `lr_ref_${paramIdx.n++}`;
500
+ params[pRef] = `%${String(v)}%`;
501
+ parts.push(`referrer LIKE {${pRef}:String}`);
502
+ }
503
+ if (cfg.utm_source) {
504
+ const p = `lr_src_${paramIdx.n++}`;
505
+ params[p] = `%${String(cfg.utm_source)}%`;
506
+ parts.push(`utm_source LIKE {${p}:String}`);
507
+ }
508
+ if (cfg.utm_medium) {
509
+ const p = `lr_med_${paramIdx.n++}`;
510
+ params[p] = `%${String(cfg.utm_medium)}%`;
511
+ parts.push(`utm_medium LIKE {${p}:String}`);
512
+ }
513
+ if (cfg.utm_campaign) {
514
+ const p = `lr_camp_${paramIdx.n++}`;
515
+ params[p] = `%${String(cfg.utm_campaign)}%`;
516
+ parts.push(`utm_campaign LIKE {${p}:String}`);
517
+ }
518
+ }
519
+ return parts.join(' AND ');
520
+ }
521
+ _resolveWindowBounds(rule, cfg) {
522
+ if (rule.time) {
523
+ if (rule.time.unit && rule.time.value != null) {
524
+ const unit = String(rule.time.unit);
525
+ const value = Number(rule.time.value);
526
+ const unitMs = {
527
+ minutes: 60000, hours: 3600000, days: 86400000, weeks: 604800000, months: 2592000000
528
+ };
529
+ const from = new Date(Date.now() - value * (unitMs[unit] ?? unitMs.days));
530
+ return { start: from.toISOString(), end: new Date().toISOString() };
531
+ }
532
+ return { start: rule.time.from ? String(rule.time.from) : undefined, end: rule.time.to ? String(rule.time.to) : undefined };
533
+ }
534
+ if (cfg?.timeFrame) {
535
+ const tf = String(cfg.timeFrame).trim();
536
+ let unit = 'days';
537
+ let value = 7;
538
+ if (/^\d+$/.test(tf)) {
539
+ value = Number(tf);
540
+ unit = 'days';
541
+ }
542
+ else if (/^\d+\s*m$/.test(tf)) {
543
+ value = Number(tf.replace(/m$/, ''));
544
+ unit = 'minutes';
545
+ }
546
+ else if (/^\d+\s*h$/.test(tf)) {
547
+ value = Number(tf.replace(/h$/, ''));
548
+ unit = 'hours';
549
+ }
550
+ else if (/^\d+\s*d$/.test(tf)) {
551
+ value = Number(tf.replace(/d$/, ''));
552
+ unit = 'days';
553
+ }
554
+ else if (/^\d+\s*w$/.test(tf)) {
555
+ value = Number(tf.replace(/w$/, ''));
556
+ unit = 'weeks';
557
+ }
558
+ else if (/^\d+\s*mo$/.test(tf)) {
559
+ value = Number(tf.replace(/mo$/, ''));
560
+ unit = 'months';
561
+ }
562
+ const unitMs = {
563
+ minutes: 60000, hours: 3600000, days: 86400000, weeks: 604800000, months: 2592000000
564
+ };
565
+ const from = new Date(Date.now() - value * (unitMs[unit] ?? unitMs.days));
566
+ return { start: from.toISOString(), end: new Date().toISOString() };
567
+ }
568
+ return {};
569
+ }
570
+ async _queryToContactIdSet(query, params, reachyIdToContactId, allContactIds) {
571
+ const result = await this.client.query({ query, query_params: params, format: 'JSONEachRow' });
572
+ const rows = await result.json();
573
+ const res = new Set();
574
+ for (const row of rows) {
575
+ const cid = resolveContactId(row, reachyIdToContactId, allContactIds);
576
+ if (cid)
577
+ res.add(cid);
578
+ }
579
+ return res;
580
+ }
581
+ }
582
+ exports.ClickHouseEventQueryExecutor = ClickHouseEventQueryExecutor;
583
+ function getEventPropertyValue(eventData, keyRaw) {
584
+ const key = String(keyRaw || '').trim();
585
+ if (!key)
586
+ return undefined;
587
+ const ed = eventData || {};
588
+ if (key.startsWith('event_data.'))
589
+ return getByPath(ed, key.replace(/^event_data\./, ''));
590
+ if (key.startsWith('custom_data.'))
591
+ return getByPath(ed?.custom_data, key.replace(/^custom_data\./, ''));
592
+ return getByPath(ed, key) ?? getByPath(ed?.custom_data, key);
593
+ }
594
+ function getByPath(obj, dottedPath) {
595
+ if (!obj || !dottedPath)
596
+ return undefined;
597
+ let cur = obj;
598
+ for (const p of dottedPath.split('.').filter(Boolean)) {
599
+ if (cur == null)
600
+ return undefined;
601
+ cur = cur[p];
602
+ }
603
+ return cur;
604
+ }
605
+ function matchesEventPropertyFilter(raw, opRaw, expected) {
606
+ const op = String(opRaw ?? '').trim();
607
+ if (op === 'is_empty')
608
+ return raw === null || raw === undefined || String(raw).trim() === '';
609
+ if (op === 'is_not_empty')
610
+ return raw !== null && raw !== undefined && String(raw).trim() !== '';
611
+ if (raw === null || raw === undefined)
612
+ return false;
613
+ const a = String(raw);
614
+ const b = String(expected ?? '');
615
+ if (op === 'equals')
616
+ return a === b;
617
+ if (op === 'not_equals')
618
+ return a !== b;
619
+ if (op === 'contains')
620
+ return a.toLowerCase().includes(b.toLowerCase());
621
+ if (op === 'not_contains')
622
+ return !a.toLowerCase().includes(b.toLowerCase());
623
+ if (op === 'starts_with')
624
+ return a.toLowerCase().startsWith(b.toLowerCase());
625
+ if (op === 'ends_with')
626
+ return a.toLowerCase().endsWith(b.toLowerCase());
627
+ const aNum = Number(raw);
628
+ const bNum = Number(expected);
629
+ if (Number.isFinite(aNum) && Number.isFinite(bNum)) {
630
+ if (op === 'gt')
631
+ return aNum > bNum;
632
+ if (op === 'gte')
633
+ return aNum >= bNum;
634
+ if (op === 'lt')
635
+ return aNum < bNum;
636
+ if (op === 'lte')
637
+ return aNum <= bNum;
638
+ }
639
+ return false;
640
+ }
641
+ function parseTimeToMinutes(raw) {
642
+ const s = String(raw || '').trim();
643
+ const m = s.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
644
+ if (!m)
645
+ return null;
646
+ let hh = Number(m[1]);
647
+ const mm = Number(m[2]);
648
+ const ampm = m[3] ? String(m[3]).toUpperCase() : null;
649
+ if (ampm === 'AM')
650
+ hh = hh === 12 ? 0 : hh;
651
+ if (ampm === 'PM')
652
+ hh = hh === 12 ? 12 : hh + 12;
653
+ return hh * 60 + mm;
654
+ }
655
+ function getLocalTimeParts(iso, tz) {
656
+ const d = new Date(String(iso || ''));
657
+ if (Number.isNaN(d.getTime()))
658
+ return null;
659
+ const dtf = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false, weekday: 'short', day: '2-digit' });
660
+ const parts = dtf.formatToParts(d);
661
+ let hourStr = '', minStr = '', weekdayStr = '', dayStr = '';
662
+ for (const p of parts) {
663
+ if (p.type === 'hour')
664
+ hourStr = p.value;
665
+ if (p.type === 'minute')
666
+ minStr = p.value;
667
+ if (p.type === 'weekday')
668
+ weekdayStr = p.value;
669
+ if (p.type === 'day')
670
+ dayStr = p.value;
671
+ }
672
+ const hour = Number(hourStr);
673
+ const minute = Number(minStr);
674
+ const dayOfMonth = Number(dayStr);
675
+ if (!Number.isFinite(hour) || !Number.isFinite(minute) || !Number.isFinite(dayOfMonth))
676
+ return null;
677
+ const minutes = hour * 60 + minute;
678
+ const weekdayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
679
+ const weekday = weekdayMap[weekdayStr] ?? -1;
680
+ if (weekday < 0)
681
+ return null;
682
+ return { minutes, weekday, dayOfMonth };
683
+ }
684
+ function parseWeekdayToIndex(raw) {
685
+ const s = String(raw || '').trim().toLowerCase();
686
+ const map = { sunday: 0, sun: 0, monday: 1, mon: 1, tuesday: 2, tue: 2, wednesday: 3, wed: 3, thursday: 4, thu: 4, friday: 5, fri: 5, saturday: 6, sat: 6 };
687
+ return map[s] ?? null;
688
+ }
689
+ function applyEventRuleFilters(rows, filters, opts) {
690
+ let out = rows;
691
+ const tz = String(opts?.timezone || '').trim();
692
+ for (const f of filters) {
693
+ const type = String(f?.type || '').trim();
694
+ if (type === 'event_property') {
695
+ const key = String(f?.key || '').trim();
696
+ if (!key)
697
+ continue;
698
+ out = out.filter((row) => matchesEventPropertyFilter(getEventPropertyValue(row?.event_data, key), f?.op, f?.value));
699
+ }
700
+ else if (type === 'time_of_day') {
701
+ if (!tz)
702
+ continue;
703
+ const startMin = parseTimeToMinutes(f?.timeStart);
704
+ const endMin = parseTimeToMinutes(f?.timeEnd);
705
+ if (startMin == null || endMin == null || startMin > endMin) {
706
+ out = [];
707
+ continue;
708
+ }
709
+ out = out.filter((row) => { const p = getLocalTimeParts(row?.event_timestamp, tz); return p ? p.minutes >= startMin && p.minutes <= endMin : false; });
710
+ }
711
+ else if (type === 'day_of_week') {
712
+ if (!tz)
713
+ continue;
714
+ const allowed = new Set();
715
+ for (const d of Array.isArray(f?.days) ? f.days : []) {
716
+ const idx = parseWeekdayToIndex(d);
717
+ if (idx != null)
718
+ allowed.add(idx);
719
+ }
720
+ if (allowed.size === 0)
721
+ continue;
722
+ out = out.filter((row) => { const p = getLocalTimeParts(row?.event_timestamp, tz); return p ? allowed.has(p.weekday) : false; });
723
+ }
724
+ else if (type === 'day_of_month') {
725
+ if (!tz)
726
+ continue;
727
+ const allowed = new Set();
728
+ for (const d of Array.isArray(f?.days) ? f.days : []) {
729
+ const n = Number(d);
730
+ if (Number.isFinite(n) && n >= 1 && n <= 31)
731
+ allowed.add(Math.floor(n));
732
+ }
733
+ if (allowed.size === 0)
734
+ continue;
735
+ out = out.filter((row) => { const p = getLocalTimeParts(row?.event_timestamp, tz); return p ? allowed.has(p.dayOfMonth) : false; });
736
+ }
737
+ }
738
+ return out;
739
+ }
740
+ function matchesInterest(raw, op, expected) {
741
+ if (raw === null || raw === undefined)
742
+ return false;
743
+ const a = String(raw).trim();
744
+ const b = String(expected ?? '').trim();
745
+ if (!b)
746
+ return false;
747
+ const nOp = String(op || 'equals').trim();
748
+ if (nOp === 'equals')
749
+ return a === b;
750
+ if (nOp === 'contains')
751
+ return a.toLowerCase().includes(b.toLowerCase());
752
+ if (nOp === 'starts_with')
753
+ return a.toLowerCase().startsWith(b.toLowerCase());
754
+ if (nOp === 'ends_with')
755
+ return a.toLowerCase().endsWith(b.toLowerCase());
756
+ if (nOp === 'not_equals')
757
+ return a !== b;
758
+ return a.toLowerCase().includes(b.toLowerCase());
759
+ }
760
+ function computeInterestContactIds(rows, interest, resolveId) {
761
+ const { key, op, value, occurrenceType = 'predominantly', occurrencePercentage = 0 } = interest;
762
+ const threshold = Number.isFinite(occurrencePercentage) ? Math.max(0, Math.min(100, occurrencePercentage)) / 100 : 0;
763
+ const totals = new Map();
764
+ const matchCounts = new Map();
765
+ const valueCounts = new Map();
766
+ for (const row of rows) {
767
+ const id = resolveId(row);
768
+ if (!id)
769
+ continue;
770
+ totals.set(id, (totals.get(id) || 0) + 1);
771
+ const rawVal = getEventPropertyValue(row?.event_data, key);
772
+ const strVal = rawVal === null || rawVal === undefined ? '' : String(rawVal);
773
+ if (!valueCounts.has(id))
774
+ valueCounts.set(id, new Map());
775
+ const per = valueCounts.get(id);
776
+ per.set(strVal, (per.get(strVal) || 0) + 1);
777
+ if (matchesInterest(rawVal, op, value))
778
+ matchCounts.set(id, (matchCounts.get(id) || 0) + 1);
779
+ }
780
+ const res = new Set();
781
+ totals.forEach((total, id) => {
782
+ if (total <= 0)
783
+ return;
784
+ const match = matchCounts.get(id) || 0;
785
+ if (occurrenceType === 'at_least') {
786
+ if (match > 0 && match / total >= threshold)
787
+ res.add(id);
788
+ return;
789
+ }
790
+ const per = valueCounts.get(id);
791
+ if (!per)
792
+ return;
793
+ let maxOther = 0;
794
+ per.forEach((cnt, valStr) => { if (!matchesInterest(valStr, op, value)) {
795
+ if (cnt > maxOther)
796
+ maxOther = cnt;
797
+ } });
798
+ if (match > 0 && match >= maxOther)
799
+ res.add(id);
800
+ });
801
+ return res;
802
+ }
803
+ //# sourceMappingURL=ClickHouseEventQueryExecutor.js.map