agrs-sequelize-sdk 1.4.52 → 1.4.53
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 +152 -152
- 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 -163
- 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 -0
- package/models/CampaignTagAssignment.js +48 -0
- 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 +40 -40
- package/models/MineChannel.js +42 -42
- package/models/MineRedirectDomain.js +36 -36
- 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/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/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 +190 -185
- 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,166 +1,166 @@
|
|
|
1
|
-
module.exports = (sequelize, DataTypes) => {
|
|
2
|
-
// public_api_batch_items — one durable work row per campaign inside a bulk
|
|
3
|
-
// batch. This is the queue the bounded-concurrency poller claims from:
|
|
4
|
-
// UPDATE public_api_batch_items
|
|
5
|
-
// SET status='PROCESSING', lease_until = now() + interval 'N minutes'
|
|
6
|
-
// WHERE id IN (SELECT id FROM public_api_batch_items
|
|
7
|
-
// WHERE status='PENDING' ORDER BY created_at
|
|
8
|
-
// LIMIT :n FOR UPDATE SKIP LOCKED)
|
|
9
|
-
// RETURNING id;
|
|
10
|
-
// The claim is admission control (exactly-once START across N instances), NOT
|
|
11
|
-
// mutual exclusion across the multi-minute FB round-trip — that is what
|
|
12
|
-
// `lease_until` + the in-core 0-ad / 0-campaign rechecks guard (R1/R2).
|
|
13
|
-
const PublicApiBatchItem = sequelize.define(
|
|
14
|
-
"PublicApiBatchItem",
|
|
15
|
-
{
|
|
16
|
-
id: {
|
|
17
|
-
type: DataTypes.UUID,
|
|
18
|
-
primaryKey: true,
|
|
19
|
-
defaultValue: DataTypes.UUIDV4,
|
|
20
|
-
},
|
|
21
|
-
batch_id: {
|
|
22
|
-
// Parent public_api_batches.id (UUID). No hard DB FK constraint by
|
|
23
|
-
// design — matches the ai_campaign_queue precedent: this is a
|
|
24
|
-
// high-throughput work queue where per-row FK checks and sync-time
|
|
25
|
-
// create-ordering add cost/risk for no integrity we don't already get
|
|
26
|
-
// from the atomic batch+items TXN. Association below is for querying.
|
|
27
|
-
type: DataTypes.UUID,
|
|
28
|
-
allowNull: false,
|
|
29
|
-
field: "batch_id",
|
|
30
|
-
},
|
|
31
|
-
item_index: {
|
|
32
|
-
// Position of this campaign within the submitted batch, for ordered
|
|
33
|
-
// reporting in GET /processes/{batchId}.
|
|
34
|
-
type: DataTypes.INTEGER,
|
|
35
|
-
allowNull: true,
|
|
36
|
-
field: "item_index",
|
|
37
|
-
},
|
|
38
|
-
status: {
|
|
39
|
-
type: DataTypes.STRING(50),
|
|
40
|
-
allowNull: false,
|
|
41
|
-
defaultValue: "PENDING",
|
|
42
|
-
validate: {
|
|
43
|
-
isIn: [["PENDING", "PROCESSING", "COMPLETED", "FAILED"]],
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
payload: {
|
|
47
|
-
// The single-campaign creation payload consumed by
|
|
48
|
-
// createSingleCampaign(payload, { batchItemId }). Self-contained.
|
|
49
|
-
type: DataTypes.JSONB,
|
|
50
|
-
allowNull: false,
|
|
51
|
-
},
|
|
52
|
-
process_id: {
|
|
53
|
-
// Bridge to CampaignCreationLogV2.process_id (D5). Assigned when the
|
|
54
|
-
// core starts; GET /processes joins item -> V2 log via this.
|
|
55
|
-
type: DataTypes.UUID,
|
|
56
|
-
allowNull: true,
|
|
57
|
-
field: "process_id",
|
|
58
|
-
},
|
|
59
|
-
agrs_cid: {
|
|
60
|
-
// AGRS Campaign ID — used by LOOP1 dedup and as the reclaim idempotency
|
|
61
|
-
// anchor (R2). Indexed for the campaign-level existence recheck.
|
|
62
|
-
type: DataTypes.STRING(255),
|
|
63
|
-
allowNull: true,
|
|
64
|
-
field: "agrs_cid",
|
|
65
|
-
},
|
|
66
|
-
idempotency_token: {
|
|
67
|
-
// Campaign-level idempotency anchor keyed by AGRS_CID + country group
|
|
68
|
-
// (R2). Lets a reclaimed re-entry prove "this campaign was already
|
|
69
|
-
// POSTed to FB" without a from-scratch re-create. Forward-compat now,
|
|
70
|
-
// populated when the S3 recheck lands.
|
|
71
|
-
type: DataTypes.STRING(255),
|
|
72
|
-
allowNull: true,
|
|
73
|
-
field: "idempotency_token",
|
|
74
|
-
},
|
|
75
|
-
lease_until: {
|
|
76
|
-
// R1: the REAL lease. A claimed item is owned until this instant. The
|
|
77
|
-
// reclaim sweep only re-dispatches when lease_until < now() (window set
|
|
78
|
-
// STRICTLY greater than worst-case single-creation wall time, incl.
|
|
79
|
-
// 80004 backoffs). A worker that loses its lease must abort, not write.
|
|
80
|
-
type: DataTypes.DATE,
|
|
81
|
-
allowNull: true,
|
|
82
|
-
field: "lease_until",
|
|
83
|
-
},
|
|
84
|
-
heartbeat_at: {
|
|
85
|
-
// R1 (optional gate): last liveness ping from the owning worker, so a
|
|
86
|
-
// long-but-alive creation isn't reclaimed on bare wall-clock alone.
|
|
87
|
-
type: DataTypes.DATE,
|
|
88
|
-
allowNull: true,
|
|
89
|
-
field: "heartbeat_at",
|
|
90
|
-
},
|
|
91
|
-
retry_count: {
|
|
92
|
-
// D-FAILURE: capped requeue count. Poison (exceeds cap) -> terminal
|
|
93
|
-
// FAILED, never re-looped.
|
|
94
|
-
type: DataTypes.INTEGER,
|
|
95
|
-
allowNull: false,
|
|
96
|
-
defaultValue: 0,
|
|
97
|
-
field: "retry_count",
|
|
98
|
-
},
|
|
99
|
-
error_message: {
|
|
100
|
-
type: DataTypes.TEXT,
|
|
101
|
-
allowNull: true,
|
|
102
|
-
field: "error_message",
|
|
103
|
-
},
|
|
104
|
-
error_code: {
|
|
105
|
-
// Machine-readable terminal cause (FB subcode / PG code / classifier
|
|
106
|
-
// verdict) surfaced via GET /processes.
|
|
107
|
-
type: DataTypes.STRING(100),
|
|
108
|
-
allowNull: true,
|
|
109
|
-
field: "error_code",
|
|
110
|
-
},
|
|
111
|
-
facebook_campaign_id: {
|
|
112
|
-
type: DataTypes.STRING(255),
|
|
113
|
-
allowNull: true,
|
|
114
|
-
field: "facebook_campaign_id",
|
|
115
|
-
},
|
|
116
|
-
claimed_at: {
|
|
117
|
-
type: DataTypes.DATE,
|
|
118
|
-
allowNull: true,
|
|
119
|
-
field: "claimed_at",
|
|
120
|
-
},
|
|
121
|
-
created_at: {
|
|
122
|
-
type: DataTypes.DATE,
|
|
123
|
-
allowNull: false,
|
|
124
|
-
defaultValue: DataTypes.NOW,
|
|
125
|
-
field: "created_at",
|
|
126
|
-
},
|
|
127
|
-
updated_at: {
|
|
128
|
-
type: DataTypes.DATE,
|
|
129
|
-
allowNull: true,
|
|
130
|
-
field: "updated_at",
|
|
131
|
-
},
|
|
132
|
-
completed_at: {
|
|
133
|
-
type: DataTypes.DATE,
|
|
134
|
-
allowNull: true,
|
|
135
|
-
field: "completed_at",
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
tableName: "public_api_batch_items",
|
|
140
|
-
timestamps: false,
|
|
141
|
-
// Named explicitly: prod sync runs alter:false, so any index added later
|
|
142
|
-
// to this (by-then existing) table needs a migration targeting the name.
|
|
143
|
-
indexes: [
|
|
144
|
-
{ name: "pabi_batch_id_idx", fields: ["batch_id"] },
|
|
145
|
-
{ name: "pabi_status_idx", fields: ["status"] },
|
|
146
|
-
// Composite: the reconciler roll-up filters by (batch_id, status).
|
|
147
|
-
{ name: "pabi_batch_id_status_idx", fields: ["batch_id", "status"] },
|
|
148
|
-
// The poller claim scans PENDING oldest-first: (status, created_at).
|
|
149
|
-
{ name: "pabi_status_created_at_idx", fields: ["status", "created_at"] },
|
|
150
|
-
{ name: "pabi_process_id_idx", fields: ["process_id"] },
|
|
151
|
-
{ name: "pabi_agrs_cid_idx", fields: ["agrs_cid"] },
|
|
152
|
-
{ name: "pabi_lease_until_idx", fields: ["lease_until"] },
|
|
153
|
-
],
|
|
154
|
-
}
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
PublicApiBatchItem.associate = (models) => {
|
|
158
|
-
PublicApiBatchItem.belongsTo(models.PublicApiBatch, {
|
|
159
|
-
foreignKey: "batch_id",
|
|
160
|
-
targetKey: "id",
|
|
161
|
-
as: "batch",
|
|
162
|
-
});
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
return PublicApiBatchItem;
|
|
166
|
-
};
|
|
1
|
+
module.exports = (sequelize, DataTypes) => {
|
|
2
|
+
// public_api_batch_items — one durable work row per campaign inside a bulk
|
|
3
|
+
// batch. This is the queue the bounded-concurrency poller claims from:
|
|
4
|
+
// UPDATE public_api_batch_items
|
|
5
|
+
// SET status='PROCESSING', lease_until = now() + interval 'N minutes'
|
|
6
|
+
// WHERE id IN (SELECT id FROM public_api_batch_items
|
|
7
|
+
// WHERE status='PENDING' ORDER BY created_at
|
|
8
|
+
// LIMIT :n FOR UPDATE SKIP LOCKED)
|
|
9
|
+
// RETURNING id;
|
|
10
|
+
// The claim is admission control (exactly-once START across N instances), NOT
|
|
11
|
+
// mutual exclusion across the multi-minute FB round-trip — that is what
|
|
12
|
+
// `lease_until` + the in-core 0-ad / 0-campaign rechecks guard (R1/R2).
|
|
13
|
+
const PublicApiBatchItem = sequelize.define(
|
|
14
|
+
"PublicApiBatchItem",
|
|
15
|
+
{
|
|
16
|
+
id: {
|
|
17
|
+
type: DataTypes.UUID,
|
|
18
|
+
primaryKey: true,
|
|
19
|
+
defaultValue: DataTypes.UUIDV4,
|
|
20
|
+
},
|
|
21
|
+
batch_id: {
|
|
22
|
+
// Parent public_api_batches.id (UUID). No hard DB FK constraint by
|
|
23
|
+
// design — matches the ai_campaign_queue precedent: this is a
|
|
24
|
+
// high-throughput work queue where per-row FK checks and sync-time
|
|
25
|
+
// create-ordering add cost/risk for no integrity we don't already get
|
|
26
|
+
// from the atomic batch+items TXN. Association below is for querying.
|
|
27
|
+
type: DataTypes.UUID,
|
|
28
|
+
allowNull: false,
|
|
29
|
+
field: "batch_id",
|
|
30
|
+
},
|
|
31
|
+
item_index: {
|
|
32
|
+
// Position of this campaign within the submitted batch, for ordered
|
|
33
|
+
// reporting in GET /processes/{batchId}.
|
|
34
|
+
type: DataTypes.INTEGER,
|
|
35
|
+
allowNull: true,
|
|
36
|
+
field: "item_index",
|
|
37
|
+
},
|
|
38
|
+
status: {
|
|
39
|
+
type: DataTypes.STRING(50),
|
|
40
|
+
allowNull: false,
|
|
41
|
+
defaultValue: "PENDING",
|
|
42
|
+
validate: {
|
|
43
|
+
isIn: [["PENDING", "PROCESSING", "COMPLETED", "FAILED"]],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
payload: {
|
|
47
|
+
// The single-campaign creation payload consumed by
|
|
48
|
+
// createSingleCampaign(payload, { batchItemId }). Self-contained.
|
|
49
|
+
type: DataTypes.JSONB,
|
|
50
|
+
allowNull: false,
|
|
51
|
+
},
|
|
52
|
+
process_id: {
|
|
53
|
+
// Bridge to CampaignCreationLogV2.process_id (D5). Assigned when the
|
|
54
|
+
// core starts; GET /processes joins item -> V2 log via this.
|
|
55
|
+
type: DataTypes.UUID,
|
|
56
|
+
allowNull: true,
|
|
57
|
+
field: "process_id",
|
|
58
|
+
},
|
|
59
|
+
agrs_cid: {
|
|
60
|
+
// AGRS Campaign ID — used by LOOP1 dedup and as the reclaim idempotency
|
|
61
|
+
// anchor (R2). Indexed for the campaign-level existence recheck.
|
|
62
|
+
type: DataTypes.STRING(255),
|
|
63
|
+
allowNull: true,
|
|
64
|
+
field: "agrs_cid",
|
|
65
|
+
},
|
|
66
|
+
idempotency_token: {
|
|
67
|
+
// Campaign-level idempotency anchor keyed by AGRS_CID + country group
|
|
68
|
+
// (R2). Lets a reclaimed re-entry prove "this campaign was already
|
|
69
|
+
// POSTed to FB" without a from-scratch re-create. Forward-compat now,
|
|
70
|
+
// populated when the S3 recheck lands.
|
|
71
|
+
type: DataTypes.STRING(255),
|
|
72
|
+
allowNull: true,
|
|
73
|
+
field: "idempotency_token",
|
|
74
|
+
},
|
|
75
|
+
lease_until: {
|
|
76
|
+
// R1: the REAL lease. A claimed item is owned until this instant. The
|
|
77
|
+
// reclaim sweep only re-dispatches when lease_until < now() (window set
|
|
78
|
+
// STRICTLY greater than worst-case single-creation wall time, incl.
|
|
79
|
+
// 80004 backoffs). A worker that loses its lease must abort, not write.
|
|
80
|
+
type: DataTypes.DATE,
|
|
81
|
+
allowNull: true,
|
|
82
|
+
field: "lease_until",
|
|
83
|
+
},
|
|
84
|
+
heartbeat_at: {
|
|
85
|
+
// R1 (optional gate): last liveness ping from the owning worker, so a
|
|
86
|
+
// long-but-alive creation isn't reclaimed on bare wall-clock alone.
|
|
87
|
+
type: DataTypes.DATE,
|
|
88
|
+
allowNull: true,
|
|
89
|
+
field: "heartbeat_at",
|
|
90
|
+
},
|
|
91
|
+
retry_count: {
|
|
92
|
+
// D-FAILURE: capped requeue count. Poison (exceeds cap) -> terminal
|
|
93
|
+
// FAILED, never re-looped.
|
|
94
|
+
type: DataTypes.INTEGER,
|
|
95
|
+
allowNull: false,
|
|
96
|
+
defaultValue: 0,
|
|
97
|
+
field: "retry_count",
|
|
98
|
+
},
|
|
99
|
+
error_message: {
|
|
100
|
+
type: DataTypes.TEXT,
|
|
101
|
+
allowNull: true,
|
|
102
|
+
field: "error_message",
|
|
103
|
+
},
|
|
104
|
+
error_code: {
|
|
105
|
+
// Machine-readable terminal cause (FB subcode / PG code / classifier
|
|
106
|
+
// verdict) surfaced via GET /processes.
|
|
107
|
+
type: DataTypes.STRING(100),
|
|
108
|
+
allowNull: true,
|
|
109
|
+
field: "error_code",
|
|
110
|
+
},
|
|
111
|
+
facebook_campaign_id: {
|
|
112
|
+
type: DataTypes.STRING(255),
|
|
113
|
+
allowNull: true,
|
|
114
|
+
field: "facebook_campaign_id",
|
|
115
|
+
},
|
|
116
|
+
claimed_at: {
|
|
117
|
+
type: DataTypes.DATE,
|
|
118
|
+
allowNull: true,
|
|
119
|
+
field: "claimed_at",
|
|
120
|
+
},
|
|
121
|
+
created_at: {
|
|
122
|
+
type: DataTypes.DATE,
|
|
123
|
+
allowNull: false,
|
|
124
|
+
defaultValue: DataTypes.NOW,
|
|
125
|
+
field: "created_at",
|
|
126
|
+
},
|
|
127
|
+
updated_at: {
|
|
128
|
+
type: DataTypes.DATE,
|
|
129
|
+
allowNull: true,
|
|
130
|
+
field: "updated_at",
|
|
131
|
+
},
|
|
132
|
+
completed_at: {
|
|
133
|
+
type: DataTypes.DATE,
|
|
134
|
+
allowNull: true,
|
|
135
|
+
field: "completed_at",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
tableName: "public_api_batch_items",
|
|
140
|
+
timestamps: false,
|
|
141
|
+
// Named explicitly: prod sync runs alter:false, so any index added later
|
|
142
|
+
// to this (by-then existing) table needs a migration targeting the name.
|
|
143
|
+
indexes: [
|
|
144
|
+
{ name: "pabi_batch_id_idx", fields: ["batch_id"] },
|
|
145
|
+
{ name: "pabi_status_idx", fields: ["status"] },
|
|
146
|
+
// Composite: the reconciler roll-up filters by (batch_id, status).
|
|
147
|
+
{ name: "pabi_batch_id_status_idx", fields: ["batch_id", "status"] },
|
|
148
|
+
// The poller claim scans PENDING oldest-first: (status, created_at).
|
|
149
|
+
{ name: "pabi_status_created_at_idx", fields: ["status", "created_at"] },
|
|
150
|
+
{ name: "pabi_process_id_idx", fields: ["process_id"] },
|
|
151
|
+
{ name: "pabi_agrs_cid_idx", fields: ["agrs_cid"] },
|
|
152
|
+
{ name: "pabi_lease_until_idx", fields: ["lease_until"] },
|
|
153
|
+
],
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
PublicApiBatchItem.associate = (models) => {
|
|
158
|
+
PublicApiBatchItem.belongsTo(models.PublicApiBatch, {
|
|
159
|
+
foreignKey: "batch_id",
|
|
160
|
+
targetKey: "id",
|
|
161
|
+
as: "batch",
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return PublicApiBatchItem;
|
|
166
|
+
};
|
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
module.exports = (sequelize, DataTypes) => {
|
|
2
|
-
const PublicApiIdempotencyCache = sequelize.define(
|
|
3
|
-
"PublicApiIdempotencyCache",
|
|
4
|
-
{
|
|
5
|
-
id: {
|
|
6
|
-
type: DataTypes.UUID,
|
|
7
|
-
defaultValue: DataTypes.UUIDV4,
|
|
8
|
-
primaryKey: true,
|
|
9
|
-
},
|
|
10
|
-
// Scope every cached response to the buyer's user. Two different
|
|
11
|
-
// buyers using the same Idempotency-Key value must NOT collide, so
|
|
12
|
-
// the uniqueness index below is (user_id, key), not (key) alone.
|
|
13
|
-
user_id: {
|
|
14
|
-
type: DataTypes.UUID,
|
|
15
|
-
allowNull: false,
|
|
16
|
-
references: {
|
|
17
|
-
model: "Users",
|
|
18
|
-
key: "id",
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
// Which specific token issued the original request. Useful for
|
|
22
|
-
// support/debug ("was it the same token retrying?") but not part
|
|
23
|
-
// of uniqueness — a buyer rotating tokens mid-retry should still
|
|
24
|
-
// hit the cached response.
|
|
25
|
-
token_id: {
|
|
26
|
-
type: DataTypes.UUID,
|
|
27
|
-
allowNull: true,
|
|
28
|
-
references: {
|
|
29
|
-
model: "ApiToken",
|
|
30
|
-
key: "id",
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
// Client-supplied Idempotency-Key header. Buyer controls the value;
|
|
34
|
-
// we just store it and dedupe on it.
|
|
35
|
-
key: {
|
|
36
|
-
type: DataTypes.STRING(255),
|
|
37
|
-
allowNull: false,
|
|
38
|
-
},
|
|
39
|
-
// SHA-256 hex of the canonicalized request body. Used to reject
|
|
40
|
-
// "same key + different body" (Stripe-style strict mode) with 422.
|
|
41
|
-
// Canonicalization = JSON.stringify with sorted keys, so field-order
|
|
42
|
-
// differences don't cause a false conflict.
|
|
43
|
-
request_hash: {
|
|
44
|
-
type: DataTypes.STRING(64),
|
|
45
|
-
allowNull: false,
|
|
46
|
-
},
|
|
47
|
-
// HTTP method + path recorded for debug + admin views. Not used for
|
|
48
|
-
// dedup logic — dedup is by (user_id, key). Different endpoints
|
|
49
|
-
// reusing the same key is a client bug we surface via debug, not
|
|
50
|
-
// silently accommodate.
|
|
51
|
-
method: {
|
|
52
|
-
type: DataTypes.STRING(10),
|
|
53
|
-
allowNull: false,
|
|
54
|
-
},
|
|
55
|
-
path: {
|
|
56
|
-
type: DataTypes.STRING,
|
|
57
|
-
allowNull: false,
|
|
58
|
-
},
|
|
59
|
-
// NULL while the original request is still in-flight. Set to the
|
|
60
|
-
// final HTTP status on completion. A row with status_code IS NULL
|
|
61
|
-
// that hasn't expired means "another instance is currently
|
|
62
|
-
// processing this key" → we return 409 idempotency_inflight to
|
|
63
|
-
// the second caller so they retry after a beat.
|
|
64
|
-
status_code: {
|
|
65
|
-
type: DataTypes.INTEGER,
|
|
66
|
-
allowNull: true,
|
|
67
|
-
},
|
|
68
|
-
// Cached response body — replayed verbatim on retry hit.
|
|
69
|
-
response_body: {
|
|
70
|
-
type: DataTypes.JSONB,
|
|
71
|
-
allowNull: true,
|
|
72
|
-
},
|
|
73
|
-
// Cached response headers we want to replay (e.g. X-Request-Id
|
|
74
|
-
// from the original, Location, etc). Optional — most replays are
|
|
75
|
-
// fine without.
|
|
76
|
-
response_headers: {
|
|
77
|
-
type: DataTypes.JSONB,
|
|
78
|
-
allowNull: true,
|
|
79
|
-
},
|
|
80
|
-
// Explicit expiry column so the TTL-sweep cron/pg_cron can
|
|
81
|
-
// `DELETE WHERE expires_at < NOW()` without computing from
|
|
82
|
-
// created_at. Defaults to created_at + 24h; controller sets this
|
|
83
|
-
// when inserting so we don't rely on a DB trigger.
|
|
84
|
-
expires_at: {
|
|
85
|
-
type: DataTypes.DATE,
|
|
86
|
-
allowNull: false,
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
tableName: "PublicApiIdempotencyCache",
|
|
91
|
-
indexes: [
|
|
92
|
-
// Primary lookup: (user_id, key). UNIQUE so Postgres resolves N
|
|
93
|
-
// concurrent claims atomically — the loser hits a unique violation,
|
|
94
|
-
// Sequelize's findOrCreate catches it, re-finds the winner's row, and
|
|
95
|
-
// returns created=false. That is what makes exactly ONE of the "9
|
|
96
|
-
// concurrent retries" execute the work while the rest get 409.
|
|
97
|
-
//
|
|
98
|
-
// THIS INDEX IS THE ENTIRE FEATURE. Without it every concurrent INSERT
|
|
99
|
-
// succeeds and every retry re-runs the operation. Verified in prod on
|
|
100
|
-
// 2026-07-12: index absent -> a 9-way burst gave 5 executions; index
|
|
101
|
-
// present (local) -> exactly 1.
|
|
102
|
-
//
|
|
103
|
-
// Named explicitly so the migration that backfills it on already-created
|
|
104
|
-
// tables targets the same index. sequelize.sync() runs with alter:false
|
|
105
|
-
// in prod (server.js gates it on DB_COLUMN_TYPE_UPDATE), so it creates
|
|
106
|
-
// the table but does NOT add indexes to a table that already exists.
|
|
107
|
-
{
|
|
108
|
-
name: "PublicApiIdempotencyCache_user_id_key_uniq",
|
|
109
|
-
unique: true,
|
|
110
|
-
fields: ["user_id", "key"],
|
|
111
|
-
},
|
|
112
|
-
// Sweep index — cron does `DELETE WHERE expires_at < NOW()`. Not
|
|
113
|
-
// partial (would need to update after every insert); regular
|
|
114
|
-
// btree is fine for the sweep pattern.
|
|
115
|
-
{
|
|
116
|
-
name: "PublicApiIdempotencyCache_expires_at_idx",
|
|
117
|
-
fields: ["expires_at"],
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
}
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
PublicApiIdempotencyCache.associate = (models) => {
|
|
124
|
-
PublicApiIdempotencyCache.belongsTo(models.Users, {
|
|
125
|
-
foreignKey: "user_id",
|
|
126
|
-
as: "user",
|
|
127
|
-
});
|
|
128
|
-
PublicApiIdempotencyCache.belongsTo(models.ApiToken, {
|
|
129
|
-
foreignKey: "token_id",
|
|
130
|
-
as: "token",
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
return PublicApiIdempotencyCache;
|
|
135
|
-
};
|
|
1
|
+
module.exports = (sequelize, DataTypes) => {
|
|
2
|
+
const PublicApiIdempotencyCache = sequelize.define(
|
|
3
|
+
"PublicApiIdempotencyCache",
|
|
4
|
+
{
|
|
5
|
+
id: {
|
|
6
|
+
type: DataTypes.UUID,
|
|
7
|
+
defaultValue: DataTypes.UUIDV4,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
},
|
|
10
|
+
// Scope every cached response to the buyer's user. Two different
|
|
11
|
+
// buyers using the same Idempotency-Key value must NOT collide, so
|
|
12
|
+
// the uniqueness index below is (user_id, key), not (key) alone.
|
|
13
|
+
user_id: {
|
|
14
|
+
type: DataTypes.UUID,
|
|
15
|
+
allowNull: false,
|
|
16
|
+
references: {
|
|
17
|
+
model: "Users",
|
|
18
|
+
key: "id",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
// Which specific token issued the original request. Useful for
|
|
22
|
+
// support/debug ("was it the same token retrying?") but not part
|
|
23
|
+
// of uniqueness — a buyer rotating tokens mid-retry should still
|
|
24
|
+
// hit the cached response.
|
|
25
|
+
token_id: {
|
|
26
|
+
type: DataTypes.UUID,
|
|
27
|
+
allowNull: true,
|
|
28
|
+
references: {
|
|
29
|
+
model: "ApiToken",
|
|
30
|
+
key: "id",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
// Client-supplied Idempotency-Key header. Buyer controls the value;
|
|
34
|
+
// we just store it and dedupe on it.
|
|
35
|
+
key: {
|
|
36
|
+
type: DataTypes.STRING(255),
|
|
37
|
+
allowNull: false,
|
|
38
|
+
},
|
|
39
|
+
// SHA-256 hex of the canonicalized request body. Used to reject
|
|
40
|
+
// "same key + different body" (Stripe-style strict mode) with 422.
|
|
41
|
+
// Canonicalization = JSON.stringify with sorted keys, so field-order
|
|
42
|
+
// differences don't cause a false conflict.
|
|
43
|
+
request_hash: {
|
|
44
|
+
type: DataTypes.STRING(64),
|
|
45
|
+
allowNull: false,
|
|
46
|
+
},
|
|
47
|
+
// HTTP method + path recorded for debug + admin views. Not used for
|
|
48
|
+
// dedup logic — dedup is by (user_id, key). Different endpoints
|
|
49
|
+
// reusing the same key is a client bug we surface via debug, not
|
|
50
|
+
// silently accommodate.
|
|
51
|
+
method: {
|
|
52
|
+
type: DataTypes.STRING(10),
|
|
53
|
+
allowNull: false,
|
|
54
|
+
},
|
|
55
|
+
path: {
|
|
56
|
+
type: DataTypes.STRING,
|
|
57
|
+
allowNull: false,
|
|
58
|
+
},
|
|
59
|
+
// NULL while the original request is still in-flight. Set to the
|
|
60
|
+
// final HTTP status on completion. A row with status_code IS NULL
|
|
61
|
+
// that hasn't expired means "another instance is currently
|
|
62
|
+
// processing this key" → we return 409 idempotency_inflight to
|
|
63
|
+
// the second caller so they retry after a beat.
|
|
64
|
+
status_code: {
|
|
65
|
+
type: DataTypes.INTEGER,
|
|
66
|
+
allowNull: true,
|
|
67
|
+
},
|
|
68
|
+
// Cached response body — replayed verbatim on retry hit.
|
|
69
|
+
response_body: {
|
|
70
|
+
type: DataTypes.JSONB,
|
|
71
|
+
allowNull: true,
|
|
72
|
+
},
|
|
73
|
+
// Cached response headers we want to replay (e.g. X-Request-Id
|
|
74
|
+
// from the original, Location, etc). Optional — most replays are
|
|
75
|
+
// fine without.
|
|
76
|
+
response_headers: {
|
|
77
|
+
type: DataTypes.JSONB,
|
|
78
|
+
allowNull: true,
|
|
79
|
+
},
|
|
80
|
+
// Explicit expiry column so the TTL-sweep cron/pg_cron can
|
|
81
|
+
// `DELETE WHERE expires_at < NOW()` without computing from
|
|
82
|
+
// created_at. Defaults to created_at + 24h; controller sets this
|
|
83
|
+
// when inserting so we don't rely on a DB trigger.
|
|
84
|
+
expires_at: {
|
|
85
|
+
type: DataTypes.DATE,
|
|
86
|
+
allowNull: false,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
tableName: "PublicApiIdempotencyCache",
|
|
91
|
+
indexes: [
|
|
92
|
+
// Primary lookup: (user_id, key). UNIQUE so Postgres resolves N
|
|
93
|
+
// concurrent claims atomically — the loser hits a unique violation,
|
|
94
|
+
// Sequelize's findOrCreate catches it, re-finds the winner's row, and
|
|
95
|
+
// returns created=false. That is what makes exactly ONE of the "9
|
|
96
|
+
// concurrent retries" execute the work while the rest get 409.
|
|
97
|
+
//
|
|
98
|
+
// THIS INDEX IS THE ENTIRE FEATURE. Without it every concurrent INSERT
|
|
99
|
+
// succeeds and every retry re-runs the operation. Verified in prod on
|
|
100
|
+
// 2026-07-12: index absent -> a 9-way burst gave 5 executions; index
|
|
101
|
+
// present (local) -> exactly 1.
|
|
102
|
+
//
|
|
103
|
+
// Named explicitly so the migration that backfills it on already-created
|
|
104
|
+
// tables targets the same index. sequelize.sync() runs with alter:false
|
|
105
|
+
// in prod (server.js gates it on DB_COLUMN_TYPE_UPDATE), so it creates
|
|
106
|
+
// the table but does NOT add indexes to a table that already exists.
|
|
107
|
+
{
|
|
108
|
+
name: "PublicApiIdempotencyCache_user_id_key_uniq",
|
|
109
|
+
unique: true,
|
|
110
|
+
fields: ["user_id", "key"],
|
|
111
|
+
},
|
|
112
|
+
// Sweep index — cron does `DELETE WHERE expires_at < NOW()`. Not
|
|
113
|
+
// partial (would need to update after every insert); regular
|
|
114
|
+
// btree is fine for the sweep pattern.
|
|
115
|
+
{
|
|
116
|
+
name: "PublicApiIdempotencyCache_expires_at_idx",
|
|
117
|
+
fields: ["expires_at"],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
PublicApiIdempotencyCache.associate = (models) => {
|
|
124
|
+
PublicApiIdempotencyCache.belongsTo(models.Users, {
|
|
125
|
+
foreignKey: "user_id",
|
|
126
|
+
as: "user",
|
|
127
|
+
});
|
|
128
|
+
PublicApiIdempotencyCache.belongsTo(models.ApiToken, {
|
|
129
|
+
foreignKey: "token_id",
|
|
130
|
+
as: "token",
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return PublicApiIdempotencyCache;
|
|
135
|
+
};
|