agrs-sequelize-sdk 1.4.59 → 1.4.60
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/migrations/2026-05-17-PIXELS-FOLLOWUP.md +85 -85
- package/migrations/2026-05-17-add-platform-to-ad.js +19 -19
- package/migrations/2026-05-17-add-platform-to-adaccount.js +19 -19
- package/migrations/2026-05-17-add-platform-to-adperformance.js +19 -19
- package/migrations/2026-05-17-add-platform-to-adset.js +19 -19
- package/migrations/2026-05-17-add-platform-to-adsetperformance.js +19 -19
- package/migrations/2026-05-17-add-platform-to-aiarticleretryqueue.js +19 -19
- package/migrations/2026-05-17-add-platform-to-aicampaignqueue.js +19 -19
- package/migrations/2026-05-17-add-platform-to-article.js +19 -19
- package/migrations/2026-05-17-add-platform-to-campaign.js +19 -19
- package/migrations/2026-05-17-add-platform-to-codefuelcampaign.js +19 -19
- package/migrations/2026-05-17-add-platform-to-dynamicfeed.js +19 -19
- package/migrations/2026-05-17-add-platform-to-facebookretryqueue.js +19 -19
- package/migrations/2026-05-17-add-platform-to-pages.js +19 -19
- package/migrations/2026-05-17-add-platform-to-presets.js +19 -19
- package/migrations/2026-05-18-add-group-id-to-ai-campaign-queue.js +18 -18
- package/migrations/2026-05-18-add-platform-code-to-creation-logs.js +65 -65
- package/migrations/2026-05-18-add-platform-code-to-pixels.js +55 -55
- package/migrations/2026-05-18-add-platform-code-to-rsoc-feed-campaigns.js +55 -55
- package/migrations/2026-05-18-add-platform-date-index-to-adperformance.js +16 -16
- package/migrations/2026-05-18-add-platform-date-index-to-adsetperformance.js +16 -16
- package/migrations/2026-05-18-add-review-status-to-ad.js +36 -36
- package/migrations/2026-05-18-create-canonical-insights.js +84 -84
- package/migrations/2026-05-18-create-snapchat-public-profiles.js +68 -68
- package/migrations/2026-05-18-create-tiktok-identities.js +71 -71
- package/migrations/2026-05-18-create-tiktok-snapchat-campaigns.js +143 -143
- package/migrations/2026-05-18-create-tt-snp-adset-ad-tables.js +309 -309
- package/migrations/RUN_ME_MANUALLY_2026-05-17.sql +99 -99
- package/migrations/add-requested-from-dashboard-to-articles.js +17 -17
- package/migrations/change-adset-name-to-text.js +79 -79
- package/models/AICampaignQueue.js +151 -151
- package/models/AIGenerationLog.js +85 -85
- package/models/AIGenerationRequest.js +212 -212
- package/models/AdAccountValues.js +25 -25
- package/models/AdHistory.js +30 -30
- package/models/AdPerformance.js +104 -104
- package/models/AdSet.js +295 -295
- package/models/AdSetHistory.js +30 -30
- package/models/AdsetPerformance.js +137 -137
- package/models/AiArticleRetryQueue.js +157 -157
- package/models/AiCreationRetryQueue.js +134 -134
- package/models/ApiAuditLog.js +127 -127
- package/models/ApiToken.js +67 -67
- package/models/Article.js +222 -222
- package/models/AutomationRule.js +179 -179
- package/models/BannerTemplate.js +129 -129
- package/models/Buyers.js +25 -25
- package/models/Campaign.js +168 -168
- package/models/CampaignActionHistory.js +101 -101
- package/models/CampaignCreationLog.js +340 -340
- package/models/CampaignCreationLogV2.js +392 -392
- package/models/CampaignHistory.js +33 -33
- package/models/CampaignTag.js +46 -46
- package/models/CampaignTagAssignment.js +48 -48
- package/models/CanonicalInsights.js +68 -68
- package/models/Channel.js +55 -55
- package/models/Domain.js +44 -44
- package/models/DynamicFeed.js +218 -218
- package/models/ExplorAdsChannel.js +61 -61
- package/models/FacebookRetryQueue.js +163 -163
- package/models/Feed.js +33 -33
- package/models/FeedArticleConfiguration.js +80 -80
- package/models/FrontStoryChannel.js +59 -59
- package/models/FrontStoryChannelV2.js +60 -60
- package/models/GenericFlowRequest.js +114 -114
- package/models/MidoWebChannel.js +47 -47
- package/models/MineBrandBlocklist.js +56 -56
- package/models/MineBrandConfig.js +26 -26
- package/models/MineChannel.js +47 -47
- package/models/MineRedirectDomain.js +42 -42
- package/models/MineWebhookEvent.js +53 -53
- package/models/Pages.js +117 -117
- package/models/PipelineExecution.js +59 -59
- package/models/PolicyDogsCreativeCache.js +50 -50
- package/models/PolicyDogsImageCache.js +30 -30
- package/models/Presets.js +40 -40
- package/models/PublicApiBatch.js +155 -155
- package/models/PublicApiBatchItem.js +166 -166
- package/models/PublicApiIdempotencyCache.js +135 -135
- package/models/RSOCFeedCampaign.js +387 -387
- package/models/RSOCFeedConfig.js +46 -46
- package/models/RSOCRedirectWsCode.js +22 -22
- package/models/RsocKeywordPerformance.js +110 -110
- package/models/RuleAction.js +90 -90
- package/models/RuleCondition.js +137 -137
- package/models/RuleExecution.js +107 -107
- package/models/RulesValues.js +56 -56
- package/models/SingularChannel.js +31 -31
- package/models/SnapchatAd.js +69 -69
- package/models/SnapchatAdSquad.js +92 -92
- package/models/SnapchatCampaign.js +69 -69
- package/models/SnapchatChannel.js +31 -31
- package/models/SnapchatPublicProfiles.js +67 -67
- package/models/SupportedLocale.js +23 -23
- package/models/SyncHistory.js +249 -249
- package/models/TTQChannel.js +42 -42
- package/models/Team.js +33 -43
- package/models/TemplateMetadata.js +260 -260
- package/models/Tier2_AdAccounts.js +110 -110
- package/models/Tier2_Assets.js +70 -70
- package/models/Tier2_BusinessManagers.js +105 -105
- package/models/Tier2_CreditLines.js +99 -99
- package/models/Tier2_Pages.js +91 -91
- package/models/Tier2_Pixels.js +82 -82
- package/models/Tier2_Tokens.js +64 -64
- package/models/Tier2_UserAdAccounts.js +83 -83
- package/models/TikTokAd.js +71 -71
- package/models/TikTokAdGroup.js +82 -82
- package/models/TikTokCampaign.js +71 -71
- package/models/TiktokChannel.js +31 -31
- package/models/TiktokIdentities.js +65 -65
- package/models/TokenRotationState.js +121 -121
- package/models/TonicRSOCKeywordPerformance.js +122 -122
- package/models/Users.js +204 -204
- package/models/Vertical.js +25 -25
- package/models/newFiles.js +137 -137
- package/package.json +21 -19
- package/run.sh +214 -214
- package/services/sequelizeService.js +110 -110
- package/tarzo-skalio-integration-docs.md +691 -691
|
@@ -1,392 +1,392 @@
|
|
|
1
|
-
// models/CampaignCreationLogV2.js
|
|
2
|
-
module.exports = (sequelize, DataTypes) => {
|
|
3
|
-
const CampaignCreationLogV2 = sequelize.define(
|
|
4
|
-
"CampaignCreationLogV2",
|
|
5
|
-
{
|
|
6
|
-
id: {
|
|
7
|
-
type: DataTypes.INTEGER,
|
|
8
|
-
primaryKey: true,
|
|
9
|
-
autoIncrement: true,
|
|
10
|
-
},
|
|
11
|
-
original_log_id: {
|
|
12
|
-
type: DataTypes.INTEGER,
|
|
13
|
-
allowNull: true,
|
|
14
|
-
unique: true,
|
|
15
|
-
comment: "ID from CampaignCreationLog (V1) - prevents duplicates",
|
|
16
|
-
},
|
|
17
|
-
process_id: {
|
|
18
|
-
type: DataTypes.UUID,
|
|
19
|
-
allowNull: false,
|
|
20
|
-
comment: "Unique ID for the creation batch process",
|
|
21
|
-
},
|
|
22
|
-
status: {
|
|
23
|
-
type: DataTypes.ENUM(
|
|
24
|
-
"STARTED",
|
|
25
|
-
"IN_PROGRESS",
|
|
26
|
-
"COMPLETED",
|
|
27
|
-
"FAILED",
|
|
28
|
-
"PARTIALLY_COMPLETED"
|
|
29
|
-
),
|
|
30
|
-
allowNull: false,
|
|
31
|
-
defaultValue: "STARTED",
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
// Location & Targeting
|
|
35
|
-
country: {
|
|
36
|
-
type: DataTypes.TEXT,
|
|
37
|
-
allowNull: true,
|
|
38
|
-
},
|
|
39
|
-
countries: {
|
|
40
|
-
type: DataTypes.ARRAY(DataTypes.TEXT),
|
|
41
|
-
allowNull: true,
|
|
42
|
-
},
|
|
43
|
-
country_type: {
|
|
44
|
-
type: DataTypes.ENUM("ww", "mix"),
|
|
45
|
-
allowNull: true,
|
|
46
|
-
comment:
|
|
47
|
-
"Computed: ww (has WW), mix (multiple countries without WW), or NULL (single country)",
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
// Campaign identifiers
|
|
51
|
-
feed_campaign_id: {
|
|
52
|
-
type: DataTypes.STRING,
|
|
53
|
-
allowNull: true,
|
|
54
|
-
},
|
|
55
|
-
facebook_campaign_id: {
|
|
56
|
-
type: DataTypes.STRING,
|
|
57
|
-
allowNull: true,
|
|
58
|
-
},
|
|
59
|
-
campaign_name: {
|
|
60
|
-
type: DataTypes.STRING(255),
|
|
61
|
-
allowNull: true,
|
|
62
|
-
},
|
|
63
|
-
facebook_campaign_name: {
|
|
64
|
-
type: DataTypes.STRING(255),
|
|
65
|
-
allowNull: true,
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
// Feed & Provider
|
|
69
|
-
feed_provider: {
|
|
70
|
-
type: DataTypes.STRING(100),
|
|
71
|
-
allowNull: true,
|
|
72
|
-
},
|
|
73
|
-
feed_name: {
|
|
74
|
-
type: DataTypes.STRING(100),
|
|
75
|
-
allowNull: true,
|
|
76
|
-
comment: "Display name (e.g., 'predicto.ai' -> 'Predicto')",
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
// User & Account
|
|
80
|
-
media_buyer: {
|
|
81
|
-
type: DataTypes.STRING(100),
|
|
82
|
-
allowNull: true,
|
|
83
|
-
},
|
|
84
|
-
assignee: {
|
|
85
|
-
type: DataTypes.STRING(100),
|
|
86
|
-
allowNull: true,
|
|
87
|
-
},
|
|
88
|
-
user_name: {
|
|
89
|
-
type: DataTypes.STRING(100),
|
|
90
|
-
allowNull: true,
|
|
91
|
-
comment: "Full name from user mapping",
|
|
92
|
-
},
|
|
93
|
-
account_id: {
|
|
94
|
-
type: DataTypes.STRING(100),
|
|
95
|
-
allowNull: true,
|
|
96
|
-
},
|
|
97
|
-
account_name: {
|
|
98
|
-
type: DataTypes.STRING(255),
|
|
99
|
-
allowNull: true,
|
|
100
|
-
comment: "From AdAccount table",
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
// Campaign metadata
|
|
104
|
-
vertical: {
|
|
105
|
-
type: DataTypes.STRING(100),
|
|
106
|
-
allowNull: true,
|
|
107
|
-
},
|
|
108
|
-
platform: {
|
|
109
|
-
type: DataTypes.STRING(50),
|
|
110
|
-
allowNull: true,
|
|
111
|
-
},
|
|
112
|
-
platform_code: {
|
|
113
|
-
type: DataTypes.ENUM("fb", "tt", "snp"),
|
|
114
|
-
allowNull: false,
|
|
115
|
-
defaultValue: "fb",
|
|
116
|
-
comment:
|
|
117
|
-
"Canonical ad platform identifier: fb=Facebook, tt=TikTok, snp=Snapchat",
|
|
118
|
-
},
|
|
119
|
-
language: {
|
|
120
|
-
type: DataTypes.STRING(50),
|
|
121
|
-
allowNull: true,
|
|
122
|
-
},
|
|
123
|
-
domain: {
|
|
124
|
-
type: DataTypes.STRING(255),
|
|
125
|
-
allowNull: true,
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
// Keywords & Content
|
|
129
|
-
keywords: {
|
|
130
|
-
type: DataTypes.ARRAY(DataTypes.TEXT),
|
|
131
|
-
allowNull: true,
|
|
132
|
-
},
|
|
133
|
-
main_keyword: {
|
|
134
|
-
type: DataTypes.STRING(255),
|
|
135
|
-
allowNull: true,
|
|
136
|
-
},
|
|
137
|
-
article_style: {
|
|
138
|
-
type: DataTypes.STRING(50),
|
|
139
|
-
allowNull: true,
|
|
140
|
-
comment: "black, blue, green, white",
|
|
141
|
-
},
|
|
142
|
-
ads_category: {
|
|
143
|
-
type: DataTypes.STRING(10),
|
|
144
|
-
allowNull: true,
|
|
145
|
-
comment: "l (Low), m (Medium), h (High)",
|
|
146
|
-
},
|
|
147
|
-
ad_title: {
|
|
148
|
-
type: DataTypes.STRING(500),
|
|
149
|
-
allowNull: true,
|
|
150
|
-
},
|
|
151
|
-
notes: {
|
|
152
|
-
type: DataTypes.TEXT,
|
|
153
|
-
allowNull: true,
|
|
154
|
-
},
|
|
155
|
-
free_text: {
|
|
156
|
-
type: DataTypes.TEXT,
|
|
157
|
-
allowNull: true,
|
|
158
|
-
},
|
|
159
|
-
|
|
160
|
-
// Facebook Campaign Settings
|
|
161
|
-
pixel_id: {
|
|
162
|
-
type: DataTypes.STRING(100),
|
|
163
|
-
allowNull: true,
|
|
164
|
-
},
|
|
165
|
-
pixel_name: {
|
|
166
|
-
type: DataTypes.STRING(255),
|
|
167
|
-
allowNull: true,
|
|
168
|
-
},
|
|
169
|
-
pixel_event: {
|
|
170
|
-
type: DataTypes.STRING(50),
|
|
171
|
-
allowNull: true,
|
|
172
|
-
},
|
|
173
|
-
bid_strategy: {
|
|
174
|
-
type: DataTypes.STRING(100),
|
|
175
|
-
allowNull: true,
|
|
176
|
-
},
|
|
177
|
-
daily_budget: {
|
|
178
|
-
type: DataTypes.DECIMAL(10, 2),
|
|
179
|
-
allowNull: true,
|
|
180
|
-
},
|
|
181
|
-
bid_amount: {
|
|
182
|
-
type: DataTypes.DECIMAL(10, 2),
|
|
183
|
-
allowNull: true,
|
|
184
|
-
},
|
|
185
|
-
optimization_goal: {
|
|
186
|
-
type: DataTypes.STRING(100),
|
|
187
|
-
allowNull: true,
|
|
188
|
-
},
|
|
189
|
-
special_ad_categories: {
|
|
190
|
-
type: DataTypes.ARRAY(DataTypes.TEXT),
|
|
191
|
-
allowNull: true,
|
|
192
|
-
comment: "Array of categories (CREDIT, HOUSING, EMPLOYMENT)",
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
// Page info
|
|
196
|
-
page_id: {
|
|
197
|
-
type: DataTypes.UUID,
|
|
198
|
-
allowNull: true,
|
|
199
|
-
},
|
|
200
|
-
page_code: {
|
|
201
|
-
type: DataTypes.STRING(100),
|
|
202
|
-
allowNull: true,
|
|
203
|
-
},
|
|
204
|
-
page_name: {
|
|
205
|
-
type: DataTypes.STRING(255),
|
|
206
|
-
allowNull: true,
|
|
207
|
-
},
|
|
208
|
-
|
|
209
|
-
// Article/Content
|
|
210
|
-
agrs_aid: {
|
|
211
|
-
type: DataTypes.STRING(255),
|
|
212
|
-
allowNull: true,
|
|
213
|
-
comment: "Article ID",
|
|
214
|
-
},
|
|
215
|
-
article_id: {
|
|
216
|
-
type: DataTypes.STRING(255),
|
|
217
|
-
allowNull: true,
|
|
218
|
-
comment: "For Mine/Predicto feeds",
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
// Error handling
|
|
222
|
-
error_message: {
|
|
223
|
-
type: DataTypes.TEXT,
|
|
224
|
-
allowNull: true,
|
|
225
|
-
},
|
|
226
|
-
error_user_title: {
|
|
227
|
-
type: DataTypes.STRING(255),
|
|
228
|
-
allowNull: true,
|
|
229
|
-
},
|
|
230
|
-
error_code: {
|
|
231
|
-
type: DataTypes.STRING(50),
|
|
232
|
-
allowNull: true,
|
|
233
|
-
},
|
|
234
|
-
error_subcode: {
|
|
235
|
-
type: DataTypes.STRING(50),
|
|
236
|
-
allowNull: true,
|
|
237
|
-
},
|
|
238
|
-
|
|
239
|
-
// JSON fields for backward compatibility
|
|
240
|
-
details: {
|
|
241
|
-
type: DataTypes.JSONB,
|
|
242
|
-
allowNull: true,
|
|
243
|
-
comment: "For backward compatibility and complex nested data",
|
|
244
|
-
},
|
|
245
|
-
original_request_data: {
|
|
246
|
-
type: DataTypes.JSONB,
|
|
247
|
-
allowNull: true,
|
|
248
|
-
comment: "Full original request for reference",
|
|
249
|
-
},
|
|
250
|
-
facebook_response: {
|
|
251
|
-
type: DataTypes.JSONB,
|
|
252
|
-
allowNull: true,
|
|
253
|
-
comment: "Facebook API response",
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
tableName: "CampaignCreationLogsV2",
|
|
258
|
-
timestamps: true,
|
|
259
|
-
createdAt: "created_at",
|
|
260
|
-
updatedAt: "updated_at",
|
|
261
|
-
indexes: [
|
|
262
|
-
{ fields: ["original_log_id"], unique: true }, // Prevent duplicate V1 logs
|
|
263
|
-
{ fields: ["process_id"] },
|
|
264
|
-
{ fields: ["status"] },
|
|
265
|
-
{ fields: ["country"] },
|
|
266
|
-
{ fields: ["countries"], using: "gin" },
|
|
267
|
-
{ fields: ["country_type"] },
|
|
268
|
-
{ fields: ["feed_provider"] },
|
|
269
|
-
{ fields: ["media_buyer"] },
|
|
270
|
-
{ fields: ["account_id"] },
|
|
271
|
-
{ fields: ["vertical"] },
|
|
272
|
-
{ fields: ["keywords"], using: "gin" },
|
|
273
|
-
{ fields: [{ name: "created_at", order: "DESC" }] },
|
|
274
|
-
{
|
|
275
|
-
fields: [
|
|
276
|
-
"process_id",
|
|
277
|
-
"status",
|
|
278
|
-
{ name: "created_at", order: "DESC" },
|
|
279
|
-
],
|
|
280
|
-
},
|
|
281
|
-
{ fields: ["status", "country"] },
|
|
282
|
-
{ fields: ["feed_provider", "status"] },
|
|
283
|
-
{ fields: ["media_buyer", "status"] },
|
|
284
|
-
{ fields: ["account_id", "status"] },
|
|
285
|
-
{ fields: ["vertical", "feed_provider"] },
|
|
286
|
-
{ fields: ["details"], using: "gin" },
|
|
287
|
-
{ fields: ["platform_code"] },
|
|
288
|
-
// Partial indexes for the hourly Tarzo reconciler (steps 2/5 and 3/5
|
|
289
|
-
// in reconcilerService.js). Both steps filter on:
|
|
290
|
-
// status='IN_PROGRESS'
|
|
291
|
-
// AND details->>'ai_stage'='tarzo_awaiting_materialization'
|
|
292
|
-
// Predicate is equality — NOT `LIKE 'tarzo_%'` — because Postgres's
|
|
293
|
-
// partial-index prover doesn't cross equality→LIKE, so a LIKE-predicate
|
|
294
|
-
// index would be ignored (verified in prod 2026-07-19). Equality
|
|
295
|
-
// matches the queries verbatim, so the planner picks these up.
|
|
296
|
-
//
|
|
297
|
-
// Column choices:
|
|
298
|
-
// - feed_campaign_id: drives the nested-loop join to
|
|
299
|
-
// rsoc_feed_campaigns.AGRS_CID in step 2/5.
|
|
300
|
-
// - created_at: powers the stale age filter in step 3/5.
|
|
301
|
-
//
|
|
302
|
-
// Self-pruning: rows leave the index as soon as status flips to
|
|
303
|
-
// COMPLETED/FAILED, so index size stays proportional to in-flight
|
|
304
|
-
// rows only.
|
|
305
|
-
{
|
|
306
|
-
name: "idx_v2_awaiting_by_feed_campaign",
|
|
307
|
-
fields: ["feed_campaign_id"],
|
|
308
|
-
where: sequelize.literal(
|
|
309
|
-
`status = 'IN_PROGRESS' AND (details->>'ai_stage') = 'tarzo_awaiting_materialization'`
|
|
310
|
-
),
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
name: "idx_v2_awaiting_created_at",
|
|
314
|
-
fields: [{ name: "created_at", order: "DESC" }],
|
|
315
|
-
where: sequelize.literal(
|
|
316
|
-
`status = 'IN_PROGRESS' AND (details->>'ai_stage') = 'tarzo_awaiting_materialization'`
|
|
317
|
-
),
|
|
318
|
-
},
|
|
319
|
-
// Partial index for the ai_campaign_queue backfill query in step 2/5
|
|
320
|
-
// (completeAdoptedAwaitingLogs's third UPDATE). After the awaiting log
|
|
321
|
-
// flips to COMPLETED, ai_stage becomes 'tarzo_materialized_and_adopted'
|
|
322
|
-
// and the queue's facebook_campaign_id gets backfilled by joining V2
|
|
323
|
-
// to ai_campaign_queue via process_id.
|
|
324
|
-
//
|
|
325
|
-
// Without this, the query bitmap-scans by status='COMPLETED' (returns
|
|
326
|
-
// 1.2M rows), then filters ai_stage in memory (rejects 1.18M) → 175s
|
|
327
|
-
// seq scan, 60s timeout in prod (verified 2026-07-19).
|
|
328
|
-
//
|
|
329
|
-
// Composite (process_id, feed_campaign_id) supports both the join to
|
|
330
|
-
// ai_campaign_queue.creation_logs_process_id AND the further join to
|
|
331
|
-
// rsoc_feed_campaigns.AGRS_CID.
|
|
332
|
-
{
|
|
333
|
-
name: "idx_v2_adopted_by_process",
|
|
334
|
-
fields: ["process_id", "feed_campaign_id"],
|
|
335
|
-
where: sequelize.literal(
|
|
336
|
-
`status = 'COMPLETED' AND (details->>'ai_stage') = 'tarzo_materialized_and_adopted'`
|
|
337
|
-
),
|
|
338
|
-
},
|
|
339
|
-
// Partial index for the ai_campaign_queue "queue-flip-to-failed" query
|
|
340
|
-
// in step 3/5 (failStaleAwaitingLogs's third UPDATE). After a V2 log
|
|
341
|
-
// gets flipped to FAILED with ai_stage='tarzo_awaited_but_never_
|
|
342
|
-
// materialized', the corresponding queue row must also flip to
|
|
343
|
-
// 'failed' by joining V2 → ai_campaign_queue via process_id.
|
|
344
|
-
//
|
|
345
|
-
// Without this, the query used the plain status index → scanned 266K
|
|
346
|
-
// FAILED rows → filtered ai_stage in memory (kept 212) → 72.9s and
|
|
347
|
-
// hit 60s statement_timeout (verified 2026-07-19).
|
|
348
|
-
//
|
|
349
|
-
// Same shape as idx_v2_adopted_by_process, mirrored for the FAILED-
|
|
350
|
-
// never-materialized end-state.
|
|
351
|
-
{
|
|
352
|
-
name: "idx_v2_awaited_never_materialized_by_process",
|
|
353
|
-
fields: ["process_id"],
|
|
354
|
-
where: sequelize.literal(
|
|
355
|
-
`status = 'FAILED' AND (details->>'ai_stage') = 'tarzo_awaited_but_never_materialized'`
|
|
356
|
-
),
|
|
357
|
-
},
|
|
358
|
-
],
|
|
359
|
-
}
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
CampaignCreationLogV2.associate = (models) => {
|
|
363
|
-
if (models.CampaignCreationLog) {
|
|
364
|
-
CampaignCreationLogV2.belongsTo(models.CampaignCreationLog, {
|
|
365
|
-
foreignKey: "process_id",
|
|
366
|
-
targetKey: "process_id",
|
|
367
|
-
as: "OriginalLog",
|
|
368
|
-
constraints: false,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (models.AdAccount) {
|
|
373
|
-
CampaignCreationLogV2.belongsTo(models.AdAccount, {
|
|
374
|
-
foreignKey: "account_id",
|
|
375
|
-
targetKey: "AdAccountID",
|
|
376
|
-
as: "Account",
|
|
377
|
-
constraints: false,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (models.Pages) {
|
|
382
|
-
CampaignCreationLogV2.belongsTo(models.Pages, {
|
|
383
|
-
foreignKey: "page_code",
|
|
384
|
-
targetKey: "code",
|
|
385
|
-
as: "Page",
|
|
386
|
-
constraints: false,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
return CampaignCreationLogV2;
|
|
392
|
-
};
|
|
1
|
+
// models/CampaignCreationLogV2.js
|
|
2
|
+
module.exports = (sequelize, DataTypes) => {
|
|
3
|
+
const CampaignCreationLogV2 = sequelize.define(
|
|
4
|
+
"CampaignCreationLogV2",
|
|
5
|
+
{
|
|
6
|
+
id: {
|
|
7
|
+
type: DataTypes.INTEGER,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
autoIncrement: true,
|
|
10
|
+
},
|
|
11
|
+
original_log_id: {
|
|
12
|
+
type: DataTypes.INTEGER,
|
|
13
|
+
allowNull: true,
|
|
14
|
+
unique: true,
|
|
15
|
+
comment: "ID from CampaignCreationLog (V1) - prevents duplicates",
|
|
16
|
+
},
|
|
17
|
+
process_id: {
|
|
18
|
+
type: DataTypes.UUID,
|
|
19
|
+
allowNull: false,
|
|
20
|
+
comment: "Unique ID for the creation batch process",
|
|
21
|
+
},
|
|
22
|
+
status: {
|
|
23
|
+
type: DataTypes.ENUM(
|
|
24
|
+
"STARTED",
|
|
25
|
+
"IN_PROGRESS",
|
|
26
|
+
"COMPLETED",
|
|
27
|
+
"FAILED",
|
|
28
|
+
"PARTIALLY_COMPLETED"
|
|
29
|
+
),
|
|
30
|
+
allowNull: false,
|
|
31
|
+
defaultValue: "STARTED",
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Location & Targeting
|
|
35
|
+
country: {
|
|
36
|
+
type: DataTypes.TEXT,
|
|
37
|
+
allowNull: true,
|
|
38
|
+
},
|
|
39
|
+
countries: {
|
|
40
|
+
type: DataTypes.ARRAY(DataTypes.TEXT),
|
|
41
|
+
allowNull: true,
|
|
42
|
+
},
|
|
43
|
+
country_type: {
|
|
44
|
+
type: DataTypes.ENUM("ww", "mix"),
|
|
45
|
+
allowNull: true,
|
|
46
|
+
comment:
|
|
47
|
+
"Computed: ww (has WW), mix (multiple countries without WW), or NULL (single country)",
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Campaign identifiers
|
|
51
|
+
feed_campaign_id: {
|
|
52
|
+
type: DataTypes.STRING,
|
|
53
|
+
allowNull: true,
|
|
54
|
+
},
|
|
55
|
+
facebook_campaign_id: {
|
|
56
|
+
type: DataTypes.STRING,
|
|
57
|
+
allowNull: true,
|
|
58
|
+
},
|
|
59
|
+
campaign_name: {
|
|
60
|
+
type: DataTypes.STRING(255),
|
|
61
|
+
allowNull: true,
|
|
62
|
+
},
|
|
63
|
+
facebook_campaign_name: {
|
|
64
|
+
type: DataTypes.STRING(255),
|
|
65
|
+
allowNull: true,
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Feed & Provider
|
|
69
|
+
feed_provider: {
|
|
70
|
+
type: DataTypes.STRING(100),
|
|
71
|
+
allowNull: true,
|
|
72
|
+
},
|
|
73
|
+
feed_name: {
|
|
74
|
+
type: DataTypes.STRING(100),
|
|
75
|
+
allowNull: true,
|
|
76
|
+
comment: "Display name (e.g., 'predicto.ai' -> 'Predicto')",
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// User & Account
|
|
80
|
+
media_buyer: {
|
|
81
|
+
type: DataTypes.STRING(100),
|
|
82
|
+
allowNull: true,
|
|
83
|
+
},
|
|
84
|
+
assignee: {
|
|
85
|
+
type: DataTypes.STRING(100),
|
|
86
|
+
allowNull: true,
|
|
87
|
+
},
|
|
88
|
+
user_name: {
|
|
89
|
+
type: DataTypes.STRING(100),
|
|
90
|
+
allowNull: true,
|
|
91
|
+
comment: "Full name from user mapping",
|
|
92
|
+
},
|
|
93
|
+
account_id: {
|
|
94
|
+
type: DataTypes.STRING(100),
|
|
95
|
+
allowNull: true,
|
|
96
|
+
},
|
|
97
|
+
account_name: {
|
|
98
|
+
type: DataTypes.STRING(255),
|
|
99
|
+
allowNull: true,
|
|
100
|
+
comment: "From AdAccount table",
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Campaign metadata
|
|
104
|
+
vertical: {
|
|
105
|
+
type: DataTypes.STRING(100),
|
|
106
|
+
allowNull: true,
|
|
107
|
+
},
|
|
108
|
+
platform: {
|
|
109
|
+
type: DataTypes.STRING(50),
|
|
110
|
+
allowNull: true,
|
|
111
|
+
},
|
|
112
|
+
platform_code: {
|
|
113
|
+
type: DataTypes.ENUM("fb", "tt", "snp"),
|
|
114
|
+
allowNull: false,
|
|
115
|
+
defaultValue: "fb",
|
|
116
|
+
comment:
|
|
117
|
+
"Canonical ad platform identifier: fb=Facebook, tt=TikTok, snp=Snapchat",
|
|
118
|
+
},
|
|
119
|
+
language: {
|
|
120
|
+
type: DataTypes.STRING(50),
|
|
121
|
+
allowNull: true,
|
|
122
|
+
},
|
|
123
|
+
domain: {
|
|
124
|
+
type: DataTypes.STRING(255),
|
|
125
|
+
allowNull: true,
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Keywords & Content
|
|
129
|
+
keywords: {
|
|
130
|
+
type: DataTypes.ARRAY(DataTypes.TEXT),
|
|
131
|
+
allowNull: true,
|
|
132
|
+
},
|
|
133
|
+
main_keyword: {
|
|
134
|
+
type: DataTypes.STRING(255),
|
|
135
|
+
allowNull: true,
|
|
136
|
+
},
|
|
137
|
+
article_style: {
|
|
138
|
+
type: DataTypes.STRING(50),
|
|
139
|
+
allowNull: true,
|
|
140
|
+
comment: "black, blue, green, white",
|
|
141
|
+
},
|
|
142
|
+
ads_category: {
|
|
143
|
+
type: DataTypes.STRING(10),
|
|
144
|
+
allowNull: true,
|
|
145
|
+
comment: "l (Low), m (Medium), h (High)",
|
|
146
|
+
},
|
|
147
|
+
ad_title: {
|
|
148
|
+
type: DataTypes.STRING(500),
|
|
149
|
+
allowNull: true,
|
|
150
|
+
},
|
|
151
|
+
notes: {
|
|
152
|
+
type: DataTypes.TEXT,
|
|
153
|
+
allowNull: true,
|
|
154
|
+
},
|
|
155
|
+
free_text: {
|
|
156
|
+
type: DataTypes.TEXT,
|
|
157
|
+
allowNull: true,
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// Facebook Campaign Settings
|
|
161
|
+
pixel_id: {
|
|
162
|
+
type: DataTypes.STRING(100),
|
|
163
|
+
allowNull: true,
|
|
164
|
+
},
|
|
165
|
+
pixel_name: {
|
|
166
|
+
type: DataTypes.STRING(255),
|
|
167
|
+
allowNull: true,
|
|
168
|
+
},
|
|
169
|
+
pixel_event: {
|
|
170
|
+
type: DataTypes.STRING(50),
|
|
171
|
+
allowNull: true,
|
|
172
|
+
},
|
|
173
|
+
bid_strategy: {
|
|
174
|
+
type: DataTypes.STRING(100),
|
|
175
|
+
allowNull: true,
|
|
176
|
+
},
|
|
177
|
+
daily_budget: {
|
|
178
|
+
type: DataTypes.DECIMAL(10, 2),
|
|
179
|
+
allowNull: true,
|
|
180
|
+
},
|
|
181
|
+
bid_amount: {
|
|
182
|
+
type: DataTypes.DECIMAL(10, 2),
|
|
183
|
+
allowNull: true,
|
|
184
|
+
},
|
|
185
|
+
optimization_goal: {
|
|
186
|
+
type: DataTypes.STRING(100),
|
|
187
|
+
allowNull: true,
|
|
188
|
+
},
|
|
189
|
+
special_ad_categories: {
|
|
190
|
+
type: DataTypes.ARRAY(DataTypes.TEXT),
|
|
191
|
+
allowNull: true,
|
|
192
|
+
comment: "Array of categories (CREDIT, HOUSING, EMPLOYMENT)",
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// Page info
|
|
196
|
+
page_id: {
|
|
197
|
+
type: DataTypes.UUID,
|
|
198
|
+
allowNull: true,
|
|
199
|
+
},
|
|
200
|
+
page_code: {
|
|
201
|
+
type: DataTypes.STRING(100),
|
|
202
|
+
allowNull: true,
|
|
203
|
+
},
|
|
204
|
+
page_name: {
|
|
205
|
+
type: DataTypes.STRING(255),
|
|
206
|
+
allowNull: true,
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// Article/Content
|
|
210
|
+
agrs_aid: {
|
|
211
|
+
type: DataTypes.STRING(255),
|
|
212
|
+
allowNull: true,
|
|
213
|
+
comment: "Article ID",
|
|
214
|
+
},
|
|
215
|
+
article_id: {
|
|
216
|
+
type: DataTypes.STRING(255),
|
|
217
|
+
allowNull: true,
|
|
218
|
+
comment: "For Mine/Predicto feeds",
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// Error handling
|
|
222
|
+
error_message: {
|
|
223
|
+
type: DataTypes.TEXT,
|
|
224
|
+
allowNull: true,
|
|
225
|
+
},
|
|
226
|
+
error_user_title: {
|
|
227
|
+
type: DataTypes.STRING(255),
|
|
228
|
+
allowNull: true,
|
|
229
|
+
},
|
|
230
|
+
error_code: {
|
|
231
|
+
type: DataTypes.STRING(50),
|
|
232
|
+
allowNull: true,
|
|
233
|
+
},
|
|
234
|
+
error_subcode: {
|
|
235
|
+
type: DataTypes.STRING(50),
|
|
236
|
+
allowNull: true,
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// JSON fields for backward compatibility
|
|
240
|
+
details: {
|
|
241
|
+
type: DataTypes.JSONB,
|
|
242
|
+
allowNull: true,
|
|
243
|
+
comment: "For backward compatibility and complex nested data",
|
|
244
|
+
},
|
|
245
|
+
original_request_data: {
|
|
246
|
+
type: DataTypes.JSONB,
|
|
247
|
+
allowNull: true,
|
|
248
|
+
comment: "Full original request for reference",
|
|
249
|
+
},
|
|
250
|
+
facebook_response: {
|
|
251
|
+
type: DataTypes.JSONB,
|
|
252
|
+
allowNull: true,
|
|
253
|
+
comment: "Facebook API response",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
tableName: "CampaignCreationLogsV2",
|
|
258
|
+
timestamps: true,
|
|
259
|
+
createdAt: "created_at",
|
|
260
|
+
updatedAt: "updated_at",
|
|
261
|
+
indexes: [
|
|
262
|
+
{ fields: ["original_log_id"], unique: true }, // Prevent duplicate V1 logs
|
|
263
|
+
{ fields: ["process_id"] },
|
|
264
|
+
{ fields: ["status"] },
|
|
265
|
+
{ fields: ["country"] },
|
|
266
|
+
{ fields: ["countries"], using: "gin" },
|
|
267
|
+
{ fields: ["country_type"] },
|
|
268
|
+
{ fields: ["feed_provider"] },
|
|
269
|
+
{ fields: ["media_buyer"] },
|
|
270
|
+
{ fields: ["account_id"] },
|
|
271
|
+
{ fields: ["vertical"] },
|
|
272
|
+
{ fields: ["keywords"], using: "gin" },
|
|
273
|
+
{ fields: [{ name: "created_at", order: "DESC" }] },
|
|
274
|
+
{
|
|
275
|
+
fields: [
|
|
276
|
+
"process_id",
|
|
277
|
+
"status",
|
|
278
|
+
{ name: "created_at", order: "DESC" },
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
{ fields: ["status", "country"] },
|
|
282
|
+
{ fields: ["feed_provider", "status"] },
|
|
283
|
+
{ fields: ["media_buyer", "status"] },
|
|
284
|
+
{ fields: ["account_id", "status"] },
|
|
285
|
+
{ fields: ["vertical", "feed_provider"] },
|
|
286
|
+
{ fields: ["details"], using: "gin" },
|
|
287
|
+
{ fields: ["platform_code"] },
|
|
288
|
+
// Partial indexes for the hourly Tarzo reconciler (steps 2/5 and 3/5
|
|
289
|
+
// in reconcilerService.js). Both steps filter on:
|
|
290
|
+
// status='IN_PROGRESS'
|
|
291
|
+
// AND details->>'ai_stage'='tarzo_awaiting_materialization'
|
|
292
|
+
// Predicate is equality — NOT `LIKE 'tarzo_%'` — because Postgres's
|
|
293
|
+
// partial-index prover doesn't cross equality→LIKE, so a LIKE-predicate
|
|
294
|
+
// index would be ignored (verified in prod 2026-07-19). Equality
|
|
295
|
+
// matches the queries verbatim, so the planner picks these up.
|
|
296
|
+
//
|
|
297
|
+
// Column choices:
|
|
298
|
+
// - feed_campaign_id: drives the nested-loop join to
|
|
299
|
+
// rsoc_feed_campaigns.AGRS_CID in step 2/5.
|
|
300
|
+
// - created_at: powers the stale age filter in step 3/5.
|
|
301
|
+
//
|
|
302
|
+
// Self-pruning: rows leave the index as soon as status flips to
|
|
303
|
+
// COMPLETED/FAILED, so index size stays proportional to in-flight
|
|
304
|
+
// rows only.
|
|
305
|
+
{
|
|
306
|
+
name: "idx_v2_awaiting_by_feed_campaign",
|
|
307
|
+
fields: ["feed_campaign_id"],
|
|
308
|
+
where: sequelize.literal(
|
|
309
|
+
`status = 'IN_PROGRESS' AND (details->>'ai_stage') = 'tarzo_awaiting_materialization'`
|
|
310
|
+
),
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "idx_v2_awaiting_created_at",
|
|
314
|
+
fields: [{ name: "created_at", order: "DESC" }],
|
|
315
|
+
where: sequelize.literal(
|
|
316
|
+
`status = 'IN_PROGRESS' AND (details->>'ai_stage') = 'tarzo_awaiting_materialization'`
|
|
317
|
+
),
|
|
318
|
+
},
|
|
319
|
+
// Partial index for the ai_campaign_queue backfill query in step 2/5
|
|
320
|
+
// (completeAdoptedAwaitingLogs's third UPDATE). After the awaiting log
|
|
321
|
+
// flips to COMPLETED, ai_stage becomes 'tarzo_materialized_and_adopted'
|
|
322
|
+
// and the queue's facebook_campaign_id gets backfilled by joining V2
|
|
323
|
+
// to ai_campaign_queue via process_id.
|
|
324
|
+
//
|
|
325
|
+
// Without this, the query bitmap-scans by status='COMPLETED' (returns
|
|
326
|
+
// 1.2M rows), then filters ai_stage in memory (rejects 1.18M) → 175s
|
|
327
|
+
// seq scan, 60s timeout in prod (verified 2026-07-19).
|
|
328
|
+
//
|
|
329
|
+
// Composite (process_id, feed_campaign_id) supports both the join to
|
|
330
|
+
// ai_campaign_queue.creation_logs_process_id AND the further join to
|
|
331
|
+
// rsoc_feed_campaigns.AGRS_CID.
|
|
332
|
+
{
|
|
333
|
+
name: "idx_v2_adopted_by_process",
|
|
334
|
+
fields: ["process_id", "feed_campaign_id"],
|
|
335
|
+
where: sequelize.literal(
|
|
336
|
+
`status = 'COMPLETED' AND (details->>'ai_stage') = 'tarzo_materialized_and_adopted'`
|
|
337
|
+
),
|
|
338
|
+
},
|
|
339
|
+
// Partial index for the ai_campaign_queue "queue-flip-to-failed" query
|
|
340
|
+
// in step 3/5 (failStaleAwaitingLogs's third UPDATE). After a V2 log
|
|
341
|
+
// gets flipped to FAILED with ai_stage='tarzo_awaited_but_never_
|
|
342
|
+
// materialized', the corresponding queue row must also flip to
|
|
343
|
+
// 'failed' by joining V2 → ai_campaign_queue via process_id.
|
|
344
|
+
//
|
|
345
|
+
// Without this, the query used the plain status index → scanned 266K
|
|
346
|
+
// FAILED rows → filtered ai_stage in memory (kept 212) → 72.9s and
|
|
347
|
+
// hit 60s statement_timeout (verified 2026-07-19).
|
|
348
|
+
//
|
|
349
|
+
// Same shape as idx_v2_adopted_by_process, mirrored for the FAILED-
|
|
350
|
+
// never-materialized end-state.
|
|
351
|
+
{
|
|
352
|
+
name: "idx_v2_awaited_never_materialized_by_process",
|
|
353
|
+
fields: ["process_id"],
|
|
354
|
+
where: sequelize.literal(
|
|
355
|
+
`status = 'FAILED' AND (details->>'ai_stage') = 'tarzo_awaited_but_never_materialized'`
|
|
356
|
+
),
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
CampaignCreationLogV2.associate = (models) => {
|
|
363
|
+
if (models.CampaignCreationLog) {
|
|
364
|
+
CampaignCreationLogV2.belongsTo(models.CampaignCreationLog, {
|
|
365
|
+
foreignKey: "process_id",
|
|
366
|
+
targetKey: "process_id",
|
|
367
|
+
as: "OriginalLog",
|
|
368
|
+
constraints: false,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (models.AdAccount) {
|
|
373
|
+
CampaignCreationLogV2.belongsTo(models.AdAccount, {
|
|
374
|
+
foreignKey: "account_id",
|
|
375
|
+
targetKey: "AdAccountID",
|
|
376
|
+
as: "Account",
|
|
377
|
+
constraints: false,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (models.Pages) {
|
|
382
|
+
CampaignCreationLogV2.belongsTo(models.Pages, {
|
|
383
|
+
foreignKey: "page_code",
|
|
384
|
+
targetKey: "code",
|
|
385
|
+
as: "Page",
|
|
386
|
+
constraints: false,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
return CampaignCreationLogV2;
|
|
392
|
+
};
|