@reachy/audience-module 1.0.2 → 1.0.3
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/.gitlab-ci.yml +138 -0
- package/package.json +1 -1
- package/src/AudienceModule.ts +353 -0
- package/src/builders/CriteriaParser.ts +88 -0
- package/src/builders/QueryBuilder.ts +231 -0
- package/src/executors/StaticAudienceExecutor.ts +105 -0
- package/src/index.ts +6 -0
- package/src/types/index.ts +112 -0
- package/tsconfig.json +26 -0
package/.gitlab-ci.yml
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
image: node:20
|
|
2
|
+
|
|
3
|
+
variables:
|
|
4
|
+
GIT_DEPTH: 0
|
|
5
|
+
GIT_STRATEGY: fetch
|
|
6
|
+
|
|
7
|
+
stages:
|
|
8
|
+
- test
|
|
9
|
+
- build
|
|
10
|
+
- tag
|
|
11
|
+
- publish
|
|
12
|
+
- notify
|
|
13
|
+
|
|
14
|
+
.slack-notify-template:
|
|
15
|
+
image: alpine:3.20
|
|
16
|
+
before_script:
|
|
17
|
+
- apk add --no-cache curl jq
|
|
18
|
+
- |
|
|
19
|
+
slack_post () {
|
|
20
|
+
[ -z "${SLACK_WEBHOOK_URL:-}" ] && { echo "SLACK_WEBHOOK_URL não configurada; pulando notificação."; return 0; }
|
|
21
|
+
local color="$1" title="$2" emoji="$3" msg="$4"
|
|
22
|
+
local short_sha="${CI_COMMIT_SHA:0:8}"
|
|
23
|
+
[ -f build_info.env ] && set -a && . build_info.env && set +a || true
|
|
24
|
+
payload="$(
|
|
25
|
+
jq -n \
|
|
26
|
+
--arg ch "${SLACK_CHANNEL:-}" \
|
|
27
|
+
--arg color "$color" --arg title "$title" --arg emoji "$emoji" \
|
|
28
|
+
--arg project "$CI_PROJECT_PATH" --arg branch "$CI_COMMIT_REF_NAME" \
|
|
29
|
+
--arg sha "$short_sha" --arg actor "${GITLAB_USER_NAME:-ci}" \
|
|
30
|
+
--arg job_url "$CI_JOB_URL" --arg pipe_url "$CI_PIPELINE_URL" \
|
|
31
|
+
--arg image "${NEW_IMAGE:-${IMAGE_REPO:-}<unknown>:${IMAGE_TAG:-}}" \
|
|
32
|
+
--arg service "${PORTAINER_SERVICE_NAME:-<desconhecido>}" \
|
|
33
|
+
--arg msg "$msg" '
|
|
34
|
+
def base:
|
|
35
|
+
{ attachments: [ { color: $color, blocks: [
|
|
36
|
+
{ "type":"header", "text": { "type":"plain_text", "text": ($emoji+" "+$title) } },
|
|
37
|
+
{ "type":"section", "fields": [
|
|
38
|
+
{ "type":"mrkdwn", "text": ("*Projeto:*\n"+$project) },
|
|
39
|
+
{ "type":"mrkdwn", "text": ("*Branch:*\n"+$branch) },
|
|
40
|
+
{ "type":"mrkdwn", "text": ("*Commit:*\n"+$sha) },
|
|
41
|
+
{ "type":"mrkdwn", "text": ("*Autor:*\n"+$actor) },
|
|
42
|
+
{ "type":"mrkdwn", "text": ("*Serviço:*\n"+$service) },
|
|
43
|
+
{ "type":"mrkdwn", "text": ("*Imagem:*\n"+$image) }
|
|
44
|
+
] },
|
|
45
|
+
{ "type":"actions", "elements": [
|
|
46
|
+
{ "type":"button", "text": { "type":"plain_text","text":"Ver Job" }, "url": $job_url },
|
|
47
|
+
{ "type":"button", "text": { "type":"plain_text","text":"Ver Pipeline" }, "url": $pipe_url }
|
|
48
|
+
] }
|
|
49
|
+
] } ] };
|
|
50
|
+
def add_msg(obj):
|
|
51
|
+
if ($msg|length) > 0 then obj | .attachments[0].blocks += [ { "type":"context", "elements":[ { "type":"mrkdwn", "text": $msg } ] } ] else obj end;
|
|
52
|
+
base | (if ($ch|length) > 0 then . + {channel:$ch} else . end) | add_msg(.)
|
|
53
|
+
'
|
|
54
|
+
)"
|
|
55
|
+
curl -sS -X POST -H 'Content-type: application/json' --data "$payload" "$SLACK_WEBHOOK_URL" >/dev/null || true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
default:
|
|
59
|
+
cache:
|
|
60
|
+
key: ${CI_COMMIT_REF_SLUG}
|
|
61
|
+
paths:
|
|
62
|
+
- node_modules/
|
|
63
|
+
before_script:
|
|
64
|
+
- npm ci
|
|
65
|
+
tags:
|
|
66
|
+
- docker
|
|
67
|
+
|
|
68
|
+
lint_and_test:
|
|
69
|
+
stage: test
|
|
70
|
+
script:
|
|
71
|
+
- npm test -- --watch=false || echo "No tests configured"
|
|
72
|
+
rules:
|
|
73
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
74
|
+
|
|
75
|
+
build:
|
|
76
|
+
stage: build
|
|
77
|
+
script:
|
|
78
|
+
- npm run build
|
|
79
|
+
artifacts:
|
|
80
|
+
paths:
|
|
81
|
+
- dist/
|
|
82
|
+
needs:
|
|
83
|
+
- lint_and_test
|
|
84
|
+
rules:
|
|
85
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
86
|
+
|
|
87
|
+
create_tag:
|
|
88
|
+
stage: tag
|
|
89
|
+
needs:
|
|
90
|
+
- build
|
|
91
|
+
before_script: []
|
|
92
|
+
script:
|
|
93
|
+
- git config --global user.email "${GITLAB_USER_EMAIL:-ci@gitlab}"
|
|
94
|
+
- git config --global user.name "${GITLAB_USER_NAME:-gitlab-ci}"
|
|
95
|
+
- git fetch --all --tags
|
|
96
|
+
- VERSION=$(node -p "require('./package.json').version")
|
|
97
|
+
- TAG="v${VERSION}"
|
|
98
|
+
- if git rev-parse "$TAG" >/dev/null 2>&1; then echo "Tag $TAG already exists, skipping"; exit 0; fi
|
|
99
|
+
- |
|
|
100
|
+
if [ -z "${GITLAB_TOKEN:-}" ]; then
|
|
101
|
+
echo "GITLAB_TOKEN não configurado com permissão write_repository; não é possível criar tag."
|
|
102
|
+
exit 1
|
|
103
|
+
fi
|
|
104
|
+
- |
|
|
105
|
+
AUTH_USER="oauth2"
|
|
106
|
+
AUTH_TOKEN="$GITLAB_TOKEN"
|
|
107
|
+
REPO_URL="https://${AUTH_USER}:${AUTH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
|
|
108
|
+
git tag "$TAG"
|
|
109
|
+
- git push "$REPO_URL" "$TAG"
|
|
110
|
+
rules:
|
|
111
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
112
|
+
|
|
113
|
+
publish_npm:
|
|
114
|
+
stage: publish
|
|
115
|
+
needs:
|
|
116
|
+
- job: build
|
|
117
|
+
artifacts: true
|
|
118
|
+
- create_tag
|
|
119
|
+
script:
|
|
120
|
+
- echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
|
121
|
+
- npm publish --access public
|
|
122
|
+
rules:
|
|
123
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
124
|
+
when: manual
|
|
125
|
+
allow_failure: false
|
|
126
|
+
|
|
127
|
+
notify:publish:
|
|
128
|
+
stage: notify
|
|
129
|
+
extends: [.slack-notify-template]
|
|
130
|
+
needs:
|
|
131
|
+
- job: publish_npm
|
|
132
|
+
rules:
|
|
133
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
134
|
+
when: on_success
|
|
135
|
+
- when: never
|
|
136
|
+
script:
|
|
137
|
+
- slack_post "#2EB67D" "Publicação npm concluída" ":white_check_mark:" \
|
|
138
|
+
"Pacote publicado a partir do branch main. Versão \`$(node -p \"require('./package.json').version\")\`."
|
package/package.json
CHANGED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { StaticAudienceExecutor } from './executors/StaticAudienceExecutor'
|
|
2
|
+
import { CriteriaParser } from './builders/CriteriaParser'
|
|
3
|
+
import {
|
|
4
|
+
AudienceCriteria,
|
|
5
|
+
AudienceQueryOptions,
|
|
6
|
+
AudienceExecutionResult,
|
|
7
|
+
AudienceBuilderConfig,
|
|
8
|
+
CreateAudienceData,
|
|
9
|
+
UpdateAudienceData
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Módulo centralizado para todas as operações de Audience
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const audienceModule = new AudienceModule()
|
|
18
|
+
*
|
|
19
|
+
* // Configurar repositórios necessários
|
|
20
|
+
* audienceModule.setRepositories({
|
|
21
|
+
* contactRepository,
|
|
22
|
+
* audienceRepository,
|
|
23
|
+
* memberRepository
|
|
24
|
+
* })
|
|
25
|
+
*
|
|
26
|
+
* // Executar query de audiência
|
|
27
|
+
* const result = await audienceModule.executeQuery(criteria, {
|
|
28
|
+
* organizationId: 'org-123',
|
|
29
|
+
* projectId: 'proj-456'
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class AudienceModule {
|
|
34
|
+
private audienceRepository: any
|
|
35
|
+
private memberRepository: any
|
|
36
|
+
private contactRepository: any
|
|
37
|
+
private staticExecutor: StaticAudienceExecutor
|
|
38
|
+
|
|
39
|
+
constructor(config: AudienceBuilderConfig = {}) {
|
|
40
|
+
this.staticExecutor = new StaticAudienceExecutor()
|
|
41
|
+
|
|
42
|
+
// Log config se debug estiver habilitado
|
|
43
|
+
if (config.enableDebugLogs) {
|
|
44
|
+
console.log('🔧 AudienceModule configurado:', config)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Configura os repositórios necessários
|
|
50
|
+
* Deve ser chamado antes de usar o módulo
|
|
51
|
+
*/
|
|
52
|
+
setRepositories(repositories: {
|
|
53
|
+
contactRepository?: any
|
|
54
|
+
audienceRepository?: any
|
|
55
|
+
memberRepository?: any
|
|
56
|
+
}) {
|
|
57
|
+
if (repositories.contactRepository) {
|
|
58
|
+
this.contactRepository = repositories.contactRepository
|
|
59
|
+
this.staticExecutor.setContactRepository(repositories.contactRepository)
|
|
60
|
+
}
|
|
61
|
+
if (repositories.audienceRepository) {
|
|
62
|
+
this.audienceRepository = repositories.audienceRepository
|
|
63
|
+
}
|
|
64
|
+
if (repositories.memberRepository) {
|
|
65
|
+
this.memberRepository = repositories.memberRepository
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ===========================
|
|
70
|
+
// QUERY EXECUTION
|
|
71
|
+
// ===========================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Executa query de audiência e retorna resultados
|
|
75
|
+
*/
|
|
76
|
+
async executeQuery(
|
|
77
|
+
criteria: string | AudienceCriteria,
|
|
78
|
+
options: AudienceQueryOptions
|
|
79
|
+
): Promise<AudienceExecutionResult> {
|
|
80
|
+
const parsed = CriteriaParser.parse(criteria)
|
|
81
|
+
const type = CriteriaParser.getAudienceType(parsed)
|
|
82
|
+
|
|
83
|
+
if (type === 'static') {
|
|
84
|
+
return this.staticExecutor.execute(parsed, options)
|
|
85
|
+
} else {
|
|
86
|
+
return this.executeLiveQuery(parsed, options)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Retorna apenas a contagem de contatos (mais rápido)
|
|
92
|
+
*/
|
|
93
|
+
async getContactCount(
|
|
94
|
+
criteria: string | AudienceCriteria,
|
|
95
|
+
options: Omit<AudienceQueryOptions, 'pagination'>
|
|
96
|
+
): Promise<number> {
|
|
97
|
+
const parsed = CriteriaParser.parse(criteria)
|
|
98
|
+
return this.staticExecutor.executeCount(parsed, options)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Retorna IDs de contatos que atendem aos critérios
|
|
103
|
+
*/
|
|
104
|
+
async getContactIds(
|
|
105
|
+
criteria: string | AudienceCriteria,
|
|
106
|
+
organizationId: string,
|
|
107
|
+
projectId: string
|
|
108
|
+
): Promise<Set<string>> {
|
|
109
|
+
if (!this.contactRepository) {
|
|
110
|
+
throw new Error('ContactRepository não configurado')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parsed = CriteriaParser.parse(criteria)
|
|
114
|
+
return this.contactRepository.getContactIdsByAudienceCriteriaV2(
|
|
115
|
+
organizationId,
|
|
116
|
+
projectId,
|
|
117
|
+
parsed
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ===========================
|
|
122
|
+
// CRUD OPERATIONS
|
|
123
|
+
// ===========================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Cria nova audiência
|
|
127
|
+
*/
|
|
128
|
+
async createAudience(data: CreateAudienceData) {
|
|
129
|
+
if (!this.audienceRepository) {
|
|
130
|
+
throw new Error('AudienceRepository não configurado')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const validation = CriteriaParser.validate(data.criteria)
|
|
134
|
+
if (!validation.valid) {
|
|
135
|
+
throw new Error(`Critérios inválidos: ${validation.errors.join(', ')}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { data: audience, error } = await this.audienceRepository.create(data)
|
|
139
|
+
if (error) throw error
|
|
140
|
+
|
|
141
|
+
const type = CriteriaParser.getAudienceType(data.criteria)
|
|
142
|
+
if (type === 'static') {
|
|
143
|
+
const count = await this.getContactCount(data.criteria, {
|
|
144
|
+
organizationId: data.organization_id,
|
|
145
|
+
projectId: data.project_id
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
await this.audienceRepository.updateCount(audience!.id, count)
|
|
149
|
+
audience!.count = count
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { data: audience, error: null }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Atualiza audiência existente
|
|
157
|
+
*/
|
|
158
|
+
async updateAudience(
|
|
159
|
+
id: string,
|
|
160
|
+
updateData: UpdateAudienceData,
|
|
161
|
+
organizationId: string,
|
|
162
|
+
projectId: string
|
|
163
|
+
) {
|
|
164
|
+
if (!this.audienceRepository) {
|
|
165
|
+
throw new Error('AudienceRepository não configurado')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (updateData.criteria) {
|
|
169
|
+
const validation = CriteriaParser.validate(updateData.criteria)
|
|
170
|
+
if (!validation.valid) {
|
|
171
|
+
throw new Error(`Critérios inválidos: ${validation.errors.join(', ')}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { data: audience, error } = await this.audienceRepository.update(id, updateData)
|
|
176
|
+
if (error) throw error
|
|
177
|
+
|
|
178
|
+
if (updateData.criteria) {
|
|
179
|
+
const type = CriteriaParser.getAudienceType(updateData.criteria)
|
|
180
|
+
if (type === 'static') {
|
|
181
|
+
const count = await this.getContactCount(updateData.criteria, {
|
|
182
|
+
organizationId,
|
|
183
|
+
projectId
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
await this.audienceRepository.updateCount(id, count)
|
|
187
|
+
audience!.count = count
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { data: audience, error: null }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Busca audiência por ID
|
|
196
|
+
*/
|
|
197
|
+
async getAudienceById(id: string, organizationId: string, projectId: string) {
|
|
198
|
+
if (!this.audienceRepository) {
|
|
199
|
+
throw new Error('AudienceRepository não configurado')
|
|
200
|
+
}
|
|
201
|
+
return this.audienceRepository.findById(id, organizationId, projectId)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Deleta audiência
|
|
206
|
+
*/
|
|
207
|
+
async deleteAudience(id: string) {
|
|
208
|
+
if (!this.audienceRepository) {
|
|
209
|
+
throw new Error('AudienceRepository não configurado')
|
|
210
|
+
}
|
|
211
|
+
return this.audienceRepository.delete(id)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ===========================
|
|
215
|
+
// MEMBERS MANAGEMENT
|
|
216
|
+
// ===========================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Verifica se uma audience tem regra de evento específico
|
|
220
|
+
*/
|
|
221
|
+
hasEventRule(criteria: any, eventName: string): boolean {
|
|
222
|
+
try {
|
|
223
|
+
// Parse do critério
|
|
224
|
+
let parsed = criteria
|
|
225
|
+
if (typeof criteria === 'string') {
|
|
226
|
+
try {
|
|
227
|
+
parsed = JSON.parse(criteria)
|
|
228
|
+
} catch {
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Verificar formato V2 com groups/rules
|
|
234
|
+
if (parsed.groups && Array.isArray(parsed.groups)) {
|
|
235
|
+
for (const group of parsed.groups) {
|
|
236
|
+
if (group.rules && Array.isArray(group.rules)) {
|
|
237
|
+
const found = group.rules.some((rule: any) =>
|
|
238
|
+
rule.kind === 'event' && rule.eventName === eventName
|
|
239
|
+
)
|
|
240
|
+
if (found) return true
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return false
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error('Error checking event rule:', error)
|
|
248
|
+
return false
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Adiciona contatos à audiência (bulk)
|
|
254
|
+
*/
|
|
255
|
+
async addMembers(
|
|
256
|
+
audienceId: string,
|
|
257
|
+
contactIds: string[],
|
|
258
|
+
organizationId: string,
|
|
259
|
+
projectId: string,
|
|
260
|
+
origin: 'realtime' | 'backfill' = 'backfill'
|
|
261
|
+
) {
|
|
262
|
+
if (!this.memberRepository) {
|
|
263
|
+
throw new Error('MemberRepository não configurado')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return this.memberRepository.bulkUpsert(
|
|
267
|
+
audienceId,
|
|
268
|
+
organizationId,
|
|
269
|
+
projectId,
|
|
270
|
+
contactIds,
|
|
271
|
+
origin
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Lista membros da audiência
|
|
277
|
+
*/
|
|
278
|
+
async getMembers(
|
|
279
|
+
audienceId: string,
|
|
280
|
+
organizationId: string,
|
|
281
|
+
projectId: string,
|
|
282
|
+
pagination?: { page?: number; limit?: number }
|
|
283
|
+
) {
|
|
284
|
+
if (!this.memberRepository) {
|
|
285
|
+
throw new Error('MemberRepository não configurado')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return this.memberRepository.listMembers(
|
|
289
|
+
audienceId,
|
|
290
|
+
organizationId,
|
|
291
|
+
projectId,
|
|
292
|
+
pagination
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ===========================
|
|
297
|
+
// UTILITIES
|
|
298
|
+
// ===========================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Valida critérios de audiência
|
|
302
|
+
*/
|
|
303
|
+
validateCriteria(criteria: string | AudienceCriteria) {
|
|
304
|
+
const parsed = CriteriaParser.parse(criteria)
|
|
305
|
+
return CriteriaParser.validate(parsed)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Detecta tipo de audiência
|
|
310
|
+
*/
|
|
311
|
+
getAudienceType(criteria: string | AudienceCriteria): 'static' | 'live' {
|
|
312
|
+
const parsed = CriteriaParser.parse(criteria)
|
|
313
|
+
return CriteriaParser.getAudienceType(parsed)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ===========================
|
|
317
|
+
// PRIVATE METHODS
|
|
318
|
+
// ===========================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Executa query de audiência live
|
|
322
|
+
*/
|
|
323
|
+
private async executeLiveQuery(
|
|
324
|
+
_criteria: AudienceCriteria,
|
|
325
|
+
options: AudienceQueryOptions
|
|
326
|
+
): Promise<AudienceExecutionResult> {
|
|
327
|
+
const startTime = Date.now()
|
|
328
|
+
|
|
329
|
+
if (!this.memberRepository) {
|
|
330
|
+
throw new Error('MemberRepository não configurado para audiences live')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const { data: members, count } = await this.memberRepository.listMembers(
|
|
334
|
+
'',
|
|
335
|
+
options.organizationId,
|
|
336
|
+
options.projectId,
|
|
337
|
+
options.pagination
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
const contactIds = new Set<string>(members?.map((m: any) => m.id) || [])
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
contactIds,
|
|
344
|
+
contacts: members || [],
|
|
345
|
+
count: count || 0,
|
|
346
|
+
metadata: {
|
|
347
|
+
executionTime: Date.now() - startTime,
|
|
348
|
+
criteriaType: 'live'
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { AudienceCriteria } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parser centralizado para normalizar diferentes formatos de critérios
|
|
5
|
+
*/
|
|
6
|
+
export class CriteriaParser {
|
|
7
|
+
/**
|
|
8
|
+
* Parse e normaliza critérios para formato padrão
|
|
9
|
+
*/
|
|
10
|
+
static parse(criteria: string | AudienceCriteria): AudienceCriteria {
|
|
11
|
+
let parsed: AudienceCriteria
|
|
12
|
+
|
|
13
|
+
if (typeof criteria === 'string') {
|
|
14
|
+
try {
|
|
15
|
+
parsed = JSON.parse(criteria)
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('❌ Erro ao fazer parse de critérios:', error)
|
|
18
|
+
throw new Error('Critérios inválidos')
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
parsed = criteria
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return this.normalizeCriteria(parsed)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normaliza diferentes formatos de critérios para um formato único
|
|
29
|
+
*/
|
|
30
|
+
private static normalizeCriteria(criteria: AudienceCriteria): AudienceCriteria {
|
|
31
|
+
if (criteria.filters && Array.isArray(criteria.filters)) {
|
|
32
|
+
return {
|
|
33
|
+
...criteria,
|
|
34
|
+
conditions: criteria.filters.map(filter => ({
|
|
35
|
+
groupId: filter.id,
|
|
36
|
+
operator: filter.operator as 'AND' | 'OR',
|
|
37
|
+
conditions: filter.conditions.map(cond => ({
|
|
38
|
+
field: cond.field,
|
|
39
|
+
operator: cond.operator,
|
|
40
|
+
value: cond.value,
|
|
41
|
+
customFieldKey: cond.customFieldKey,
|
|
42
|
+
logicalOperator: cond.logicalOperator as 'AND' | 'OR' | undefined
|
|
43
|
+
}))
|
|
44
|
+
}))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (criteria.groups) {
|
|
49
|
+
return criteria
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return criteria
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detecta o tipo de audiência
|
|
57
|
+
*/
|
|
58
|
+
static getAudienceType(criteria: AudienceCriteria): 'static' | 'live' {
|
|
59
|
+
const liveTypes = ['live-actions', 'live-page-visit', 'live-referrer', 'live-page-count']
|
|
60
|
+
return liveTypes.includes(criteria.type || '') ? 'live' : 'static'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Valida se os critérios são válidos
|
|
65
|
+
*/
|
|
66
|
+
static validate(criteria: AudienceCriteria): { valid: boolean; errors: string[] } {
|
|
67
|
+
const errors: string[] = []
|
|
68
|
+
|
|
69
|
+
if (!criteria || typeof criteria !== 'object') {
|
|
70
|
+
errors.push('Critérios devem ser um objeto')
|
|
71
|
+
return { valid: false, errors }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const hasConditions = criteria.conditions && Array.isArray(criteria.conditions)
|
|
75
|
+
const hasFilters = criteria.filters && Array.isArray(criteria.filters)
|
|
76
|
+
const hasGroups = criteria.groups && Array.isArray(criteria.groups)
|
|
77
|
+
|
|
78
|
+
if (!hasConditions && !hasFilters && !hasGroups) {
|
|
79
|
+
errors.push('Critérios devem conter conditions, filters ou groups')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
valid: errors.length === 0,
|
|
84
|
+
errors
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { AudienceCriteria } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Construtor de queries para diferentes tipos de critérios
|
|
5
|
+
*/
|
|
6
|
+
export class QueryBuilder {
|
|
7
|
+
/**
|
|
8
|
+
* Constrói filtros Supabase baseado nos critérios
|
|
9
|
+
*/
|
|
10
|
+
static buildFilters(query: any, criteria: AudienceCriteria, organizationId: string, projectId: string): any {
|
|
11
|
+
query = query
|
|
12
|
+
.eq('organization_id', organizationId)
|
|
13
|
+
.eq('project_id', projectId)
|
|
14
|
+
|
|
15
|
+
return this.applyAudienceCriteria(query, criteria)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Aplica critérios de audiência ao query
|
|
20
|
+
*/
|
|
21
|
+
private static applyAudienceCriteria(query: any, criteria: AudienceCriteria): any {
|
|
22
|
+
if (criteria.groups && Array.isArray(criteria.groups)) {
|
|
23
|
+
return this.applyGroupsCriteria(query, criteria.groups)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (criteria.filters && Array.isArray(criteria.filters)) {
|
|
27
|
+
return this.applyFiltersCriteria(query, criteria.filters)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (criteria.conditions && Array.isArray(criteria.conditions)) {
|
|
31
|
+
return this.applyConditionsCriteria(query, criteria.conditions)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return query
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Aplica filtros no formato 'groups' (v2)
|
|
39
|
+
*/
|
|
40
|
+
private static applyGroupsCriteria(query: any, groups: any[]): any {
|
|
41
|
+
const orConditions: string[] = []
|
|
42
|
+
|
|
43
|
+
for (const group of groups) {
|
|
44
|
+
if (!group.rules || !Array.isArray(group.rules)) continue
|
|
45
|
+
|
|
46
|
+
const groupConditions: string[] = []
|
|
47
|
+
|
|
48
|
+
for (const rule of group.rules) {
|
|
49
|
+
const condition = this.buildConditionString(rule)
|
|
50
|
+
if (condition) {
|
|
51
|
+
groupConditions.push(condition)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (groupConditions.length > 0) {
|
|
56
|
+
orConditions.push(groupConditions.join(','))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (orConditions.length > 0) {
|
|
61
|
+
query = query.or(orConditions.join(','))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return query
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Aplica filtros no formato 'filters' (v1)
|
|
69
|
+
*/
|
|
70
|
+
private static applyFiltersCriteria(query: any, filters: any[]): any {
|
|
71
|
+
const orGroups = filters.filter(f => f.operator === 'OR')
|
|
72
|
+
const andGroups = filters.filter(f => f.operator !== 'OR')
|
|
73
|
+
|
|
74
|
+
if (orGroups.length > 0) {
|
|
75
|
+
const orConditions: string[] = []
|
|
76
|
+
|
|
77
|
+
for (const filterGroup of orGroups) {
|
|
78
|
+
const groupConditions: string[] = []
|
|
79
|
+
|
|
80
|
+
for (const condition of filterGroup.conditions) {
|
|
81
|
+
const condStr = this.buildConditionString(condition)
|
|
82
|
+
if (condStr) {
|
|
83
|
+
groupConditions.push(condStr)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (groupConditions.length > 0) {
|
|
88
|
+
orConditions.push(groupConditions.join(','))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (orConditions.length > 0) {
|
|
93
|
+
query = query.or(orConditions.join(','))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const filterGroup of andGroups) {
|
|
98
|
+
for (const condition of filterGroup.conditions) {
|
|
99
|
+
query = this.applyCondition(query, condition)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return query
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Aplica critérios no formato 'conditions' (normalizado)
|
|
108
|
+
*/
|
|
109
|
+
private static applyConditionsCriteria(query: any, conditions: any[]): any {
|
|
110
|
+
for (const group of conditions) {
|
|
111
|
+
if (group.operator === 'OR') {
|
|
112
|
+
const orConditions = group.conditions.map((c: any) => this.buildConditionString(c)).filter(Boolean)
|
|
113
|
+
if (orConditions.length > 0) {
|
|
114
|
+
query = query.or(orConditions.join(','))
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
for (const condition of group.conditions) {
|
|
118
|
+
query = this.applyCondition(query, condition)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return query
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Aplica uma condição individual ao query
|
|
128
|
+
*/
|
|
129
|
+
private static applyCondition(query: any, condition: any): any {
|
|
130
|
+
const { field, operator, value } = condition
|
|
131
|
+
|
|
132
|
+
const dbField = this.mapFieldToColumn(field, condition.customFieldKey)
|
|
133
|
+
|
|
134
|
+
switch (operator) {
|
|
135
|
+
case 'equals':
|
|
136
|
+
case 'eq':
|
|
137
|
+
return query.eq(dbField, value)
|
|
138
|
+
case 'not_equals':
|
|
139
|
+
case 'neq':
|
|
140
|
+
return query.neq(dbField, value)
|
|
141
|
+
case 'contains':
|
|
142
|
+
case 'ilike':
|
|
143
|
+
return query.ilike(dbField, `%${value}%`)
|
|
144
|
+
case 'not_contains':
|
|
145
|
+
return query.not.ilike(dbField, `%${value}%`)
|
|
146
|
+
case 'starts_with':
|
|
147
|
+
return query.ilike(dbField, `${value}%`)
|
|
148
|
+
case 'ends_with':
|
|
149
|
+
return query.ilike(dbField, `%${value}`)
|
|
150
|
+
case 'greater_than':
|
|
151
|
+
case 'gt':
|
|
152
|
+
return query.gt(dbField, value)
|
|
153
|
+
case 'less_than':
|
|
154
|
+
case 'lt':
|
|
155
|
+
return query.lt(dbField, value)
|
|
156
|
+
case 'is_empty':
|
|
157
|
+
return query.or(`${dbField}.is.null,${dbField}.eq.`)
|
|
158
|
+
case 'is_not_empty':
|
|
159
|
+
return query.not.is(dbField, null)
|
|
160
|
+
default:
|
|
161
|
+
console.warn(`⚠️ Operador desconhecido: ${operator}`)
|
|
162
|
+
return query
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Constrói string de condição para uso em .or()
|
|
168
|
+
*/
|
|
169
|
+
private static buildConditionString(condition: any): string | null {
|
|
170
|
+
const { field, operator, value } = condition
|
|
171
|
+
|
|
172
|
+
if (!field || !operator) return null
|
|
173
|
+
|
|
174
|
+
const dbField = this.mapFieldToColumn(field, condition.customFieldKey)
|
|
175
|
+
|
|
176
|
+
switch (operator) {
|
|
177
|
+
case 'equals':
|
|
178
|
+
case 'eq':
|
|
179
|
+
return `${dbField}.eq.${value}`
|
|
180
|
+
case 'not_equals':
|
|
181
|
+
case 'neq':
|
|
182
|
+
return `${dbField}.neq.${value}`
|
|
183
|
+
case 'contains':
|
|
184
|
+
case 'ilike':
|
|
185
|
+
return `${dbField}.ilike.*${value}*`
|
|
186
|
+
case 'not_contains':
|
|
187
|
+
return `${dbField}.not.ilike.*${value}*`
|
|
188
|
+
case 'starts_with':
|
|
189
|
+
return `${dbField}.ilike.${value}*`
|
|
190
|
+
case 'ends_with':
|
|
191
|
+
return `${dbField}.ilike.*${value}`
|
|
192
|
+
case 'greater_than':
|
|
193
|
+
case 'gt':
|
|
194
|
+
return `${dbField}.gt.${value}`
|
|
195
|
+
case 'less_than':
|
|
196
|
+
case 'lt':
|
|
197
|
+
return `${dbField}.lt.${value}`
|
|
198
|
+
case 'is_empty':
|
|
199
|
+
return `${dbField}.is.null`
|
|
200
|
+
case 'is_not_empty':
|
|
201
|
+
return `${dbField}.not.is.null`
|
|
202
|
+
default:
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Mapeia campo da audiência para coluna do banco
|
|
209
|
+
*/
|
|
210
|
+
private static mapFieldToColumn(field: string, customFieldKey?: string): string {
|
|
211
|
+
if (field === 'custom_field' && customFieldKey) {
|
|
212
|
+
return `properties->>${customFieldKey}`
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const fieldMap: Record<string, string> = {
|
|
216
|
+
email: 'email',
|
|
217
|
+
name: 'name',
|
|
218
|
+
phone: 'phone',
|
|
219
|
+
city: 'city',
|
|
220
|
+
state: 'state',
|
|
221
|
+
country: 'country',
|
|
222
|
+
created_at: 'created_at',
|
|
223
|
+
updated_at: 'updated_at',
|
|
224
|
+
opted_in: 'opted_in',
|
|
225
|
+
is_subscribed: 'is_subscribed'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return fieldMap[field] || field
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { AudienceCriteria, AudienceQueryOptions, AudienceExecutionResult } from '../types'
|
|
2
|
+
import { CriteriaParser } from '../builders/CriteriaParser'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Executor para audiências estáticas (baseadas em filtros)
|
|
6
|
+
*
|
|
7
|
+
* NOTA: Este executor precisa de uma instância de ContactRepository
|
|
8
|
+
* que deve ser injetada externamente quando usado no reachy-api
|
|
9
|
+
*/
|
|
10
|
+
export class StaticAudienceExecutor {
|
|
11
|
+
private contactRepository: any
|
|
12
|
+
|
|
13
|
+
constructor(contactRepository?: any) {
|
|
14
|
+
this.contactRepository = contactRepository
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Define o ContactRepository
|
|
19
|
+
*/
|
|
20
|
+
setContactRepository(repository: any) {
|
|
21
|
+
this.contactRepository = repository
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Executa query de audiência estática e retorna IDs de contatos
|
|
26
|
+
*/
|
|
27
|
+
async execute(
|
|
28
|
+
criteria: AudienceCriteria,
|
|
29
|
+
options: AudienceQueryOptions
|
|
30
|
+
): Promise<AudienceExecutionResult> {
|
|
31
|
+
const startTime = Date.now()
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (!this.contactRepository) {
|
|
35
|
+
throw new Error('ContactRepository não foi configurado. Use setContactRepository() antes de executar.')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parsed = CriteriaParser.parse(criteria)
|
|
39
|
+
const validation = CriteriaParser.validate(parsed)
|
|
40
|
+
|
|
41
|
+
if (!validation.valid) {
|
|
42
|
+
console.error('❌ Critérios inválidos:', validation.errors)
|
|
43
|
+
return {
|
|
44
|
+
contactIds: new Set(),
|
|
45
|
+
contacts: [],
|
|
46
|
+
count: 0,
|
|
47
|
+
metadata: {
|
|
48
|
+
executionTime: Date.now() - startTime,
|
|
49
|
+
criteriaType: 'static'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const contactIds = await this.contactRepository.getContactIdsByAudienceCriteriaV2(
|
|
55
|
+
options.organizationId,
|
|
56
|
+
options.projectId,
|
|
57
|
+
parsed
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
let contacts: any[] | undefined
|
|
61
|
+
|
|
62
|
+
if (options.includeCount || options.pagination) {
|
|
63
|
+
const { page = 1, limit = 10 } = options.pagination || {}
|
|
64
|
+
const contactIdsArray = Array.from(contactIds)
|
|
65
|
+
|
|
66
|
+
if (contactIdsArray.length > 0) {
|
|
67
|
+
const paginatedIds = contactIdsArray.slice((page - 1) * limit, page * limit)
|
|
68
|
+
|
|
69
|
+
const { data } = await this.contactRepository.findByIds(
|
|
70
|
+
options.organizationId,
|
|
71
|
+
options.projectId,
|
|
72
|
+
paginatedIds
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
contacts = data || []
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
contactIds,
|
|
81
|
+
contacts,
|
|
82
|
+
count: contactIds.size,
|
|
83
|
+
metadata: {
|
|
84
|
+
executionTime: Date.now() - startTime,
|
|
85
|
+
criteriaType: 'static'
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('❌ Erro ao executar audiência estática:', error)
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Executa query e retorna apenas contagem (mais rápido)
|
|
96
|
+
*/
|
|
97
|
+
async executeCount(
|
|
98
|
+
criteria: AudienceCriteria,
|
|
99
|
+
options: Omit<AudienceQueryOptions, 'pagination'>
|
|
100
|
+
): Promise<number> {
|
|
101
|
+
const result = await this.execute(criteria, { ...options, includeCount: true })
|
|
102
|
+
return result.count
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AudienceModule } from './AudienceModule'
|
|
2
|
+
export { CriteriaParser } from './builders/CriteriaParser'
|
|
3
|
+
export { QueryBuilder } from './builders/QueryBuilder'
|
|
4
|
+
export { StaticAudienceExecutor } from './executors/StaticAudienceExecutor'
|
|
5
|
+
export * from './types'
|
|
6
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Interface flexível para suportar múltiplos formatos de critérios de audience
|
|
2
|
+
export interface AudienceCriteria {
|
|
3
|
+
type?: string;
|
|
4
|
+
conditions?: Array<{
|
|
5
|
+
groupId?: string;
|
|
6
|
+
operator?: 'AND' | 'OR';
|
|
7
|
+
conditions?: Array<{
|
|
8
|
+
field: string;
|
|
9
|
+
operator: string;
|
|
10
|
+
value: any;
|
|
11
|
+
customFieldKey?: string;
|
|
12
|
+
logicalOperator?: 'AND' | 'OR';
|
|
13
|
+
}>;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
groups?: Array<{
|
|
17
|
+
id?: string;
|
|
18
|
+
operator?: 'AND' | 'OR';
|
|
19
|
+
rules?: Array<{
|
|
20
|
+
kind?: string;
|
|
21
|
+
field?: string;
|
|
22
|
+
operator?: string;
|
|
23
|
+
value?: any;
|
|
24
|
+
eventName?: string;
|
|
25
|
+
customFieldKey?: string;
|
|
26
|
+
}>;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
filters?: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
operator: string;
|
|
32
|
+
conditions: Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
field: string;
|
|
35
|
+
value: string;
|
|
36
|
+
operator: string;
|
|
37
|
+
logicalOperator: string;
|
|
38
|
+
customFieldKey?: string;
|
|
39
|
+
}>;
|
|
40
|
+
}>;
|
|
41
|
+
segmentType?: string;
|
|
42
|
+
|
|
43
|
+
config?: {
|
|
44
|
+
name?: string;
|
|
45
|
+
type?: string;
|
|
46
|
+
pageUrl?: string;
|
|
47
|
+
isActive?: boolean;
|
|
48
|
+
eventType?: string;
|
|
49
|
+
pageCount?: string;
|
|
50
|
+
timeFrame?: string;
|
|
51
|
+
conditions?: any[];
|
|
52
|
+
description?: string;
|
|
53
|
+
referrerUrl?: string;
|
|
54
|
+
behaviorCriteria?: {
|
|
55
|
+
engaged?: boolean;
|
|
56
|
+
purchased?: boolean;
|
|
57
|
+
visitedPages?: boolean;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
live_options?: {
|
|
62
|
+
include_past_behavior?: boolean;
|
|
63
|
+
window_days?: number;
|
|
64
|
+
[key: string]: any;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
isActive?: boolean;
|
|
68
|
+
[key: string]: any;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AudienceQueryOptions {
|
|
72
|
+
organizationId: string
|
|
73
|
+
projectId: string
|
|
74
|
+
pagination?: {
|
|
75
|
+
page?: number
|
|
76
|
+
limit?: number
|
|
77
|
+
}
|
|
78
|
+
includeCount?: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface AudienceExecutionResult {
|
|
82
|
+
contactIds: Set<string>
|
|
83
|
+
contacts?: any[]
|
|
84
|
+
count: number
|
|
85
|
+
metadata?: {
|
|
86
|
+
executionTime: number
|
|
87
|
+
criteriaType: string
|
|
88
|
+
cacheHit?: boolean
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface AudienceBuilderConfig {
|
|
93
|
+
enableCache?: boolean
|
|
94
|
+
cacheTimeout?: number
|
|
95
|
+
enableDebugLogs?: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CreateAudienceData {
|
|
99
|
+
name: string;
|
|
100
|
+
description?: string;
|
|
101
|
+
criteria: AudienceCriteria;
|
|
102
|
+
organization_id: string;
|
|
103
|
+
project_id: string;
|
|
104
|
+
user_id: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface UpdateAudienceData {
|
|
108
|
+
name?: string;
|
|
109
|
+
description?: string;
|
|
110
|
+
criteria?: AudienceCriteria;
|
|
111
|
+
}
|
|
112
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"moduleResolution": "node",
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"removeComments": true,
|
|
17
|
+
"sourceMap": true,
|
|
18
|
+
"noImplicitAny": false,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src/**/*"],
|
|
24
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
25
|
+
}
|
|
26
|
+
|