@canonry/canonry 4.117.2 → 4.118.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +53 -1
- package/assets/assets/AuditHistoryPanel-Yh7VEe4P.js +1 -0
- package/assets/assets/{BacklinksPage-CschVQxC.js → BacklinksPage-BmO77kMs.js} +1 -1
- package/assets/assets/{ChartPrimitives-D4782Ifx.js → ChartPrimitives-H0ICVpw_.js} +1 -1
- package/assets/assets/HistoryPage-DkLV_iRc.js +1 -0
- package/assets/assets/ProjectPage-R3odr_eA.js +7 -0
- package/assets/assets/{RunRow-ggNGlwdN.js → RunRow-BDH9agxQ.js} +1 -1
- package/assets/assets/RunsPage-D9YsEeT3.js +1 -0
- package/assets/assets/{SettingsPage-Cy9SlBJX.js → SettingsPage-o5as3Ixw.js} +1 -1
- package/assets/assets/{TrafficPage-1S2PzSHB.js → TrafficPage-BCK203aQ.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-CqXCoaod.js → TrafficSourceDetailPage-DsVMAZvS.js} +1 -1
- package/assets/assets/{arrow-left-kCK5M8Bp.js → arrow-left-B7ZR6VC9.js} +1 -1
- package/assets/assets/{extract-error-message-C82LAW6q.js → extract-error-message-DeYDUpew.js} +1 -1
- package/assets/assets/index-Bot6YiD6.css +1 -0
- package/assets/assets/index-QCFQv0xh.js +210 -0
- package/assets/assets/{trash-2-8Pe4v1T0.js → trash-2-xxHTqWbF.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-4PMMEFME.js → chunk-75UBTCKS.js} +499 -16
- package/dist/{chunk-BMPIV35C.js → chunk-CMZIPFNQ.js} +937 -104
- package/dist/{chunk-F2SRZJMY.js → chunk-HGNY5GL4.js} +384 -15
- package/dist/{chunk-EZME2J2G.js → chunk-KEZWLP4O.js} +1196 -1012
- package/dist/cli.js +319 -86
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-GNOE4J35.js → intelligence-service-FWPXMAVK.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
- package/assets/assets/ProjectPage-BSGTpBUJ.js +0 -7
- package/assets/assets/RunsPage-Bfp0K6Ry.js +0 -1
- package/assets/assets/index-DrDNyUhv.css +0 -1
- package/assets/assets/index-i-t64RXw.js +0 -210
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ADS_WRITE_SCOPE,
|
|
2
3
|
AGENT_PROVIDER_IDS,
|
|
3
4
|
AI_ENGINE_DOMAINS,
|
|
4
5
|
AI_PROVIDER_INFRA_DOMAINS,
|
|
6
|
+
AdsEntityStatuses,
|
|
7
|
+
AdsEntityTypes,
|
|
8
|
+
AdsOperationKinds,
|
|
9
|
+
AdsOperationStates,
|
|
5
10
|
AiReferralTrafficClasses,
|
|
6
11
|
AppError,
|
|
7
12
|
BacklinkSources,
|
|
@@ -38,19 +43,29 @@ import {
|
|
|
38
43
|
__export,
|
|
39
44
|
absolutizeProjectUrl,
|
|
40
45
|
actionConfidenceLabel,
|
|
46
|
+
adsAdCreateRequestSchema,
|
|
47
|
+
adsAdGroupCreateRequestSchema,
|
|
48
|
+
adsAdGroupUpdateRequestSchema,
|
|
49
|
+
adsAdUpdateRequestSchema,
|
|
50
|
+
adsCampaignCreateRequestSchema,
|
|
41
51
|
adsCampaignListResponseSchema,
|
|
52
|
+
adsCampaignUpdateRequestSchema,
|
|
42
53
|
adsConnectRequestSchema,
|
|
43
54
|
adsConnectionStatusDtoSchema,
|
|
44
55
|
adsCpcMicros,
|
|
45
56
|
adsCtr,
|
|
46
57
|
adsDisconnectResponseSchema,
|
|
58
|
+
adsImageUploadRequestSchema,
|
|
47
59
|
adsInsightLevelSchema,
|
|
48
60
|
adsInsightsResponseSchema,
|
|
61
|
+
adsOperationResponseSchema,
|
|
62
|
+
adsPauseRequestSchema,
|
|
49
63
|
adsSummaryDtoSchema,
|
|
50
64
|
adsSyncResponseSchema,
|
|
51
65
|
agentProvidersResponseDtoSchema,
|
|
52
66
|
aggregateHarvestedQueries,
|
|
53
67
|
aiReferralClassCounts,
|
|
68
|
+
alreadyExists,
|
|
54
69
|
apiKeyDtoSchema,
|
|
55
70
|
apiKeyListDtoSchema,
|
|
56
71
|
applyHarvestSemanticNovelty,
|
|
@@ -173,6 +188,7 @@ import {
|
|
|
173
188
|
gscSitemapListResponseDtoSchema,
|
|
174
189
|
gscUrlInspectionDtoSchema,
|
|
175
190
|
hasLocationLabel,
|
|
191
|
+
healthSnapshotDtoSchema,
|
|
176
192
|
hostOf,
|
|
177
193
|
indexingRequestResponseDtoSchema,
|
|
178
194
|
internalError,
|
|
@@ -283,10 +299,10 @@ import {
|
|
|
283
299
|
wordpressSchemaDeployResultDtoSchema,
|
|
284
300
|
wordpressSchemaStatusResultDtoSchema,
|
|
285
301
|
wordpressStatusDtoSchema
|
|
286
|
-
} from "./chunk-
|
|
302
|
+
} from "./chunk-KEZWLP4O.js";
|
|
287
303
|
|
|
288
304
|
// src/intelligence-service.ts
|
|
289
|
-
import { eq as eq40, desc as desc18, asc as
|
|
305
|
+
import { eq as eq40, desc as desc18, asc as asc8, and as and31, ne as ne5, or as or6, inArray as inArray15, gte as gte9, lte as lte5 } from "drizzle-orm";
|
|
290
306
|
|
|
291
307
|
// ../db/src/client.ts
|
|
292
308
|
import { mkdirSync } from "fs";
|
|
@@ -302,6 +318,7 @@ __export(schema_exports, {
|
|
|
302
318
|
adsCampaigns: () => adsCampaigns,
|
|
303
319
|
adsConnections: () => adsConnections,
|
|
304
320
|
adsInsightsDaily: () => adsInsightsDaily,
|
|
321
|
+
adsOperations: () => adsOperations,
|
|
305
322
|
agentMemory: () => agentMemory,
|
|
306
323
|
agentSessions: () => agentSessions,
|
|
307
324
|
agentToolEvents: () => agentToolEvents,
|
|
@@ -1444,11 +1461,33 @@ var adsConnections = sqliteTable("ads_connections", {
|
|
|
1444
1461
|
}, (table) => [
|
|
1445
1462
|
uniqueIndex("idx_ads_conn_project").on(table.projectId)
|
|
1446
1463
|
]);
|
|
1464
|
+
var adsOperations = sqliteTable("ads_operations", {
|
|
1465
|
+
id: text("id").primaryKey(),
|
|
1466
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1467
|
+
operationKey: text("operation_key").notNull(),
|
|
1468
|
+
requestHash: text("request_hash").notNull(),
|
|
1469
|
+
kind: text("kind").notNull(),
|
|
1470
|
+
state: text("state").notNull(),
|
|
1471
|
+
entityType: text("entity_type"),
|
|
1472
|
+
entityId: text("entity_id"),
|
|
1473
|
+
upstreamUpdatedAt: integer("upstream_updated_at"),
|
|
1474
|
+
errorCode: text("error_code"),
|
|
1475
|
+
errorMessage: text("error_message"),
|
|
1476
|
+
createdAt: text("created_at").notNull(),
|
|
1477
|
+
updatedAt: text("updated_at").notNull()
|
|
1478
|
+
}, (table) => [
|
|
1479
|
+
uniqueIndex("idx_ads_operations_project_key").on(table.projectId, table.operationKey),
|
|
1480
|
+
index("idx_ads_operations_project_created").on(table.projectId, table.createdAt),
|
|
1481
|
+
index("idx_ads_operations_project_state").on(table.projectId, table.state)
|
|
1482
|
+
]);
|
|
1447
1483
|
var adsCampaigns = sqliteTable("ads_campaigns", {
|
|
1448
1484
|
id: text("id").primaryKey(),
|
|
1449
1485
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1450
1486
|
name: text("name").notNull(),
|
|
1487
|
+
description: text("description"),
|
|
1451
1488
|
status: text("status").notNull(),
|
|
1489
|
+
startTime: integer("start_time"),
|
|
1490
|
+
endTime: integer("end_time"),
|
|
1452
1491
|
biddingType: text("bidding_type"),
|
|
1453
1492
|
dailySpendLimitMicros: integer("daily_spend_limit_micros"),
|
|
1454
1493
|
lifetimeSpendLimitMicros: integer("lifetime_spend_limit_micros"),
|
|
@@ -1465,6 +1504,7 @@ var adsAdGroups = sqliteTable("ads_ad_groups", {
|
|
|
1465
1504
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
1466
1505
|
campaignId: text("campaign_id").notNull().references(() => adsCampaigns.id, { onDelete: "cascade" }),
|
|
1467
1506
|
name: text("name").notNull(),
|
|
1507
|
+
description: text("description"),
|
|
1468
1508
|
status: text("status").notNull(),
|
|
1469
1509
|
billingEventType: text("billing_event_type"),
|
|
1470
1510
|
maxBidMicros: integer("max_bid_micros"),
|
|
@@ -3600,6 +3640,34 @@ var MIGRATION_VERSIONS = [
|
|
|
3600
3640
|
run: (tx) => {
|
|
3601
3641
|
relinkOrphanedSnapshotQueryIds(tx);
|
|
3602
3642
|
}
|
|
3643
|
+
},
|
|
3644
|
+
{
|
|
3645
|
+
version: 99,
|
|
3646
|
+
name: "ads-operator-lifecycle",
|
|
3647
|
+
statements: [
|
|
3648
|
+
`ALTER TABLE ads_campaigns ADD COLUMN description TEXT`,
|
|
3649
|
+
`ALTER TABLE ads_campaigns ADD COLUMN start_time INTEGER`,
|
|
3650
|
+
`ALTER TABLE ads_campaigns ADD COLUMN end_time INTEGER`,
|
|
3651
|
+
`ALTER TABLE ads_ad_groups ADD COLUMN description TEXT`,
|
|
3652
|
+
`CREATE TABLE IF NOT EXISTS ads_operations (
|
|
3653
|
+
id TEXT PRIMARY KEY,
|
|
3654
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3655
|
+
operation_key TEXT NOT NULL,
|
|
3656
|
+
request_hash TEXT NOT NULL,
|
|
3657
|
+
kind TEXT NOT NULL,
|
|
3658
|
+
state TEXT NOT NULL,
|
|
3659
|
+
entity_type TEXT,
|
|
3660
|
+
entity_id TEXT,
|
|
3661
|
+
upstream_updated_at INTEGER,
|
|
3662
|
+
error_code TEXT,
|
|
3663
|
+
error_message TEXT,
|
|
3664
|
+
created_at TEXT NOT NULL,
|
|
3665
|
+
updated_at TEXT NOT NULL
|
|
3666
|
+
)`,
|
|
3667
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_ads_operations_project_key ON ads_operations(project_id, operation_key)`,
|
|
3668
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_operations_project_created ON ads_operations(project_id, created_at)`,
|
|
3669
|
+
`CREATE INDEX IF NOT EXISTS idx_ads_operations_project_state ON ads_operations(project_id, state)`
|
|
3670
|
+
]
|
|
3603
3671
|
}
|
|
3604
3672
|
];
|
|
3605
3673
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -5948,6 +6016,14 @@ import fs8 from "fs";
|
|
|
5948
6016
|
import crypto2 from "crypto";
|
|
5949
6017
|
import { eq } from "drizzle-orm";
|
|
5950
6018
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
6019
|
+
function isAdsWriteOnlyKey(scopes) {
|
|
6020
|
+
const writeGrants = scopes.filter((scope) => scope === "*" || scope === "write" || scope.endsWith(".write"));
|
|
6021
|
+
return writeGrants.length === 1 && writeGrants[0] === ADS_WRITE_SCOPE;
|
|
6022
|
+
}
|
|
6023
|
+
function isAdsWriteRoute(url) {
|
|
6024
|
+
const rest = projectRouteRest(url);
|
|
6025
|
+
return rest !== null && rest.startsWith("ads/");
|
|
6026
|
+
}
|
|
5951
6027
|
function requireScope(request, scope) {
|
|
5952
6028
|
const key = request.apiKey;
|
|
5953
6029
|
if (!key) return;
|
|
@@ -6100,6 +6176,9 @@ async function authPlugin(app, opts = {}) {
|
|
|
6100
6176
|
if (isReadOnlyKey(scopes) && WRITE_METHODS.has(request.method)) {
|
|
6101
6177
|
throw forbidden("This API key is read-only and cannot perform write operations.");
|
|
6102
6178
|
}
|
|
6179
|
+
if (isAdsWriteOnlyKey(scopes) && WRITE_METHODS.has(request.method) && !isAdsWriteRoute(url)) {
|
|
6180
|
+
throw forbidden("This API key can only perform OpenAI Ads write operations.");
|
|
6181
|
+
}
|
|
6103
6182
|
enforceEmbedProjectTabs(request, opts.embedProjectTabs);
|
|
6104
6183
|
if (key.projectId) {
|
|
6105
6184
|
const match = url.match(/\/projects\/([^/?#]+)/);
|
|
@@ -7994,7 +8073,7 @@ function normalizeCompetitorList2(domains) {
|
|
|
7994
8073
|
}
|
|
7995
8074
|
|
|
7996
8075
|
// ../api-routes/src/history.ts
|
|
7997
|
-
import { and as and6,
|
|
8076
|
+
import { and as and6, asc as asc2, desc as desc2, eq as eq10, gte as gte2, inArray as inArray3 } from "drizzle-orm";
|
|
7998
8077
|
|
|
7999
8078
|
// ../api-routes/src/notification-redaction.ts
|
|
8000
8079
|
var REDACTED_URL = {
|
|
@@ -8040,12 +8119,16 @@ function redactNotificationDiff(value) {
|
|
|
8040
8119
|
async function historyRoutes(app) {
|
|
8041
8120
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
8042
8121
|
const project = resolveProject(app.db, request.params.name);
|
|
8043
|
-
const
|
|
8122
|
+
const filters = [eq10(auditLog.projectId, project.id)];
|
|
8123
|
+
addAuditHistoryFilters(filters, request.query);
|
|
8124
|
+
const rows = app.db.select().from(auditLog).where(and6(...filters)).orderBy(desc2(auditLog.createdAt)).limit(parseBoundedInt(request.query.limit, 100, 500)).offset(parseBoundedInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER)).all();
|
|
8044
8125
|
return reply.send(rows.map(formatAuditEntry));
|
|
8045
8126
|
});
|
|
8046
8127
|
app.get("/history", async (request, reply) => {
|
|
8047
8128
|
const scopedProjectId = request.apiKey?.projectId;
|
|
8048
|
-
const
|
|
8129
|
+
const filters = scopedProjectId ? [eq10(auditLog.projectId, scopedProjectId)] : [];
|
|
8130
|
+
addAuditHistoryFilters(filters, request.query);
|
|
8131
|
+
const rows = app.db.select().from(auditLog).where(filters.length > 0 ? and6(...filters) : void 0).orderBy(desc2(auditLog.createdAt)).limit(parseBoundedInt(request.query.limit, 100, 500)).offset(parseBoundedInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER)).all();
|
|
8049
8132
|
return reply.send(rows.map(formatAuditEntry));
|
|
8050
8133
|
});
|
|
8051
8134
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
@@ -8101,7 +8184,8 @@ async function historyRoutes(app) {
|
|
|
8101
8184
|
app.get("/projects/:name/timeline", async (request, reply) => {
|
|
8102
8185
|
const project = resolveProject(app.db, request.params.name);
|
|
8103
8186
|
const projectQueries = app.db.select().from(queries).where(eq10(queries.projectId, project.id)).all();
|
|
8104
|
-
const
|
|
8187
|
+
const requestedLimit = parseOptionalPositiveInt(request.query.limit, 100);
|
|
8188
|
+
const projectRuns = requestedLimit == null ? app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(asc2(runs.createdAt)).all() : app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(desc2(runs.createdAt)).limit(requestedLimit).all().reverse();
|
|
8105
8189
|
if (projectRuns.length === 0 || projectQueries.length === 0) {
|
|
8106
8190
|
return reply.send([]);
|
|
8107
8191
|
}
|
|
@@ -8251,29 +8335,23 @@ async function historyRoutes(app) {
|
|
|
8251
8335
|
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, run2)).all();
|
|
8252
8336
|
const map1 = /* @__PURE__ */ new Map();
|
|
8253
8337
|
for (const s of snaps1) {
|
|
8254
|
-
const resolved = {
|
|
8255
|
-
...s,
|
|
8256
|
-
resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
8257
|
-
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
|
|
8258
|
-
resolvedMentionState: resolveSnapshotMentionState(s, project)
|
|
8259
|
-
};
|
|
8260
8338
|
const existing = map1.get(s.queryId);
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8339
|
+
map1.set(s.queryId, {
|
|
8340
|
+
...s,
|
|
8341
|
+
query: existing?.query ?? s.query,
|
|
8342
|
+
citationState: existing?.citationState === CitationStates.cited || s.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"],
|
|
8343
|
+
resolvedAnswerMentioned: (existing?.resolvedAnswerMentioned ?? false) || resolveSnapshotAnswerMentioned(s, project)
|
|
8344
|
+
});
|
|
8264
8345
|
}
|
|
8265
8346
|
const map2 = /* @__PURE__ */ new Map();
|
|
8266
8347
|
for (const s of snaps2) {
|
|
8267
|
-
const resolved = {
|
|
8268
|
-
...s,
|
|
8269
|
-
resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
8270
|
-
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
|
|
8271
|
-
resolvedMentionState: resolveSnapshotMentionState(s, project)
|
|
8272
|
-
};
|
|
8273
8348
|
const existing = map2.get(s.queryId);
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8349
|
+
map2.set(s.queryId, {
|
|
8350
|
+
...s,
|
|
8351
|
+
query: existing?.query ?? s.query,
|
|
8352
|
+
citationState: existing?.citationState === CitationStates.cited || s.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"],
|
|
8353
|
+
resolvedAnswerMentioned: (existing?.resolvedAnswerMentioned ?? false) || resolveSnapshotAnswerMentioned(s, project)
|
|
8354
|
+
});
|
|
8277
8355
|
}
|
|
8278
8356
|
const allQueryIds = /* @__PURE__ */ new Set([...map1.keys(), ...map2.keys()]);
|
|
8279
8357
|
const diff = [...allQueryIds].map((qId) => {
|
|
@@ -8287,10 +8365,10 @@ async function historyRoutes(app) {
|
|
|
8287
8365
|
run1AnswerMentioned: s1?.resolvedAnswerMentioned ?? null,
|
|
8288
8366
|
run2AnswerMentioned: s2?.resolvedAnswerMentioned ?? null,
|
|
8289
8367
|
// Legacy aliases — same data as run{1,2}MentionState below.
|
|
8290
|
-
run1VisibilityState: s1
|
|
8291
|
-
run2VisibilityState: s2
|
|
8292
|
-
run1MentionState: s1
|
|
8293
|
-
run2MentionState: s2
|
|
8368
|
+
run1VisibilityState: s1 ? visibilityStateFromAnswerMentioned(s1.resolvedAnswerMentioned) : null,
|
|
8369
|
+
run2VisibilityState: s2 ? visibilityStateFromAnswerMentioned(s2.resolvedAnswerMentioned) : null,
|
|
8370
|
+
run1MentionState: s1 ? mentionStateFromAnswerMentioned(s1.resolvedAnswerMentioned) : null,
|
|
8371
|
+
run2MentionState: s2 ? mentionStateFromAnswerMentioned(s2.resolvedAnswerMentioned) : null,
|
|
8294
8372
|
changed: (s1?.citationState ?? null) !== (s2?.citationState ?? null),
|
|
8295
8373
|
visibilityChanged: (s1?.resolvedAnswerMentioned ?? null) !== (s2?.resolvedAnswerMentioned ?? null)
|
|
8296
8374
|
};
|
|
@@ -8307,9 +8385,28 @@ function formatAuditEntry(row) {
|
|
|
8307
8385
|
entityType: row.entityType,
|
|
8308
8386
|
entityId: row.entityId,
|
|
8309
8387
|
diff: row.diff ? row.entityType === "notification" ? redactNotificationDiff(parseJsonColumn(row.diff, null)) : parseJsonColumn(row.diff, null) : null,
|
|
8388
|
+
userAgent: row.userAgent,
|
|
8389
|
+
actorSession: row.actorSession,
|
|
8310
8390
|
createdAt: row.createdAt
|
|
8311
8391
|
};
|
|
8312
8392
|
}
|
|
8393
|
+
function addAuditHistoryFilters(filters, query) {
|
|
8394
|
+
if (query.since && !Number.isNaN(Date.parse(query.since))) filters.push(gte2(auditLog.createdAt, query.since));
|
|
8395
|
+
if (query.action) filters.push(eq10(auditLog.action, query.action));
|
|
8396
|
+
if (query.actor) filters.push(eq10(auditLog.actor, query.actor));
|
|
8397
|
+
if (query.entityType) filters.push(eq10(auditLog.entityType, query.entityType));
|
|
8398
|
+
}
|
|
8399
|
+
function parseBoundedInt(value, fallback, max) {
|
|
8400
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
8401
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
8402
|
+
return Math.min(parsed, max);
|
|
8403
|
+
}
|
|
8404
|
+
function parseOptionalPositiveInt(value, max) {
|
|
8405
|
+
if (value == null) return void 0;
|
|
8406
|
+
const parsed = Number.parseInt(value, 10);
|
|
8407
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
|
|
8408
|
+
return Math.min(parsed, max);
|
|
8409
|
+
}
|
|
8313
8410
|
|
|
8314
8411
|
// ../api-routes/src/analytics.ts
|
|
8315
8412
|
import { and as and7, eq as eq11, desc as desc3, inArray as inArray4 } from "drizzle-orm";
|
|
@@ -8973,7 +9070,7 @@ async function intelligenceRoutes(app) {
|
|
|
8973
9070
|
}
|
|
8974
9071
|
|
|
8975
9072
|
// ../api-routes/src/report.ts
|
|
8976
|
-
import { and as and12, desc as desc7, eq as eq16, gte as
|
|
9073
|
+
import { and as and12, desc as desc7, eq as eq16, gte as gte3, inArray as inArray7, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
|
|
8977
9074
|
|
|
8978
9075
|
// ../api-routes/src/content.ts
|
|
8979
9076
|
import crypto12 from "crypto";
|
|
@@ -9709,7 +9806,7 @@ function winnabilityClassRank(winnabilityClass) {
|
|
|
9709
9806
|
}
|
|
9710
9807
|
|
|
9711
9808
|
// ../api-routes/src/gsc-totals.ts
|
|
9712
|
-
import { and as and11, asc as
|
|
9809
|
+
import { and as and11, asc as asc3, eq as eq15, sql as sql6 } from "drizzle-orm";
|
|
9713
9810
|
function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
9714
9811
|
const rows = db.select({
|
|
9715
9812
|
date: gscDailyTotals.date,
|
|
@@ -9722,7 +9819,7 @@ function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
|
9722
9819
|
sql6`${gscDailyTotals.date} >= ${startDate}`,
|
|
9723
9820
|
sql6`${gscDailyTotals.date} <= ${endDate}`
|
|
9724
9821
|
)
|
|
9725
|
-
).orderBy(
|
|
9822
|
+
).orderBy(asc3(gscDailyTotals.date)).all();
|
|
9726
9823
|
return rows.map((r) => {
|
|
9727
9824
|
const position = Number(r.position);
|
|
9728
9825
|
return {
|
|
@@ -12189,7 +12286,7 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
12189
12286
|
eq16(gaAiReferrals.sourceDimension, "session")
|
|
12190
12287
|
];
|
|
12191
12288
|
if (snapshotStartDate && snapshotMaxDate) {
|
|
12192
|
-
aiConditions.push(
|
|
12289
|
+
aiConditions.push(gte3(gaAiReferrals.date, snapshotStartDate));
|
|
12193
12290
|
aiConditions.push(lte(gaAiReferrals.date, snapshotMaxDate));
|
|
12194
12291
|
}
|
|
12195
12292
|
const aiSessionRows = db.select({
|
|
@@ -12406,7 +12503,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12406
12503
|
and12(
|
|
12407
12504
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12408
12505
|
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12409
|
-
|
|
12506
|
+
gte3(crawlerEventsHourly.tsHour, windowStartIso),
|
|
12410
12507
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
12411
12508
|
)
|
|
12412
12509
|
).get()?.total ?? 0
|
|
@@ -12416,7 +12513,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12416
12513
|
and12(
|
|
12417
12514
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12418
12515
|
ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12419
|
-
|
|
12516
|
+
gte3(crawlerEventsHourly.tsHour, windowStartIso),
|
|
12420
12517
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
12421
12518
|
)
|
|
12422
12519
|
).get()?.total ?? 0
|
|
@@ -12430,7 +12527,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12430
12527
|
and12(
|
|
12431
12528
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12432
12529
|
nonSubresourceReferralPathCondition(),
|
|
12433
|
-
|
|
12530
|
+
gte3(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
12434
12531
|
exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
12435
12532
|
)
|
|
12436
12533
|
).get();
|
|
@@ -12440,7 +12537,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12440
12537
|
db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
12441
12538
|
and12(
|
|
12442
12539
|
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12443
|
-
|
|
12540
|
+
gte3(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
12444
12541
|
exclusiveEnd ? lt(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
12445
12542
|
)
|
|
12446
12543
|
).get()?.total ?? 0
|
|
@@ -12460,7 +12557,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12460
12557
|
}).from(crawlerEventsHourly).where(
|
|
12461
12558
|
and12(
|
|
12462
12559
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12463
|
-
|
|
12560
|
+
gte3(crawlerEventsHourly.tsHour, headlineStart),
|
|
12464
12561
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12465
12562
|
)
|
|
12466
12563
|
).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
|
|
@@ -12471,7 +12568,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12471
12568
|
and12(
|
|
12472
12569
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12473
12570
|
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12474
|
-
|
|
12571
|
+
gte3(crawlerEventsHourly.tsHour, priorStart),
|
|
12475
12572
|
lt(crawlerEventsHourly.tsHour, headlineStart)
|
|
12476
12573
|
)
|
|
12477
12574
|
).groupBy(crawlerEventsHourly.operator).all();
|
|
@@ -12482,7 +12579,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12482
12579
|
and12(
|
|
12483
12580
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12484
12581
|
nonSubresourceReferralPathCondition(),
|
|
12485
|
-
|
|
12582
|
+
gte3(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12486
12583
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
12487
12584
|
)
|
|
12488
12585
|
).groupBy(aiReferralEventsHourly.operator).all();
|
|
@@ -12492,7 +12589,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12492
12589
|
}).from(aiUserFetchEventsHourly).where(
|
|
12493
12590
|
and12(
|
|
12494
12591
|
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12495
|
-
|
|
12592
|
+
gte3(aiUserFetchEventsHourly.tsHour, headlineStart),
|
|
12496
12593
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
12497
12594
|
)
|
|
12498
12595
|
).groupBy(aiUserFetchEventsHourly.operator).all();
|
|
@@ -12537,7 +12634,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12537
12634
|
and12(
|
|
12538
12635
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12539
12636
|
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12540
|
-
|
|
12637
|
+
gte3(crawlerEventsHourly.tsHour, headlineStart),
|
|
12541
12638
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12542
12639
|
)
|
|
12543
12640
|
).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql7`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
@@ -12554,7 +12651,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12554
12651
|
and12(
|
|
12555
12652
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12556
12653
|
nonSubresourceReferralPathCondition(),
|
|
12557
|
-
|
|
12654
|
+
gte3(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12558
12655
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
12559
12656
|
)
|
|
12560
12657
|
).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
|
|
@@ -12571,7 +12668,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12571
12668
|
and12(
|
|
12572
12669
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12573
12670
|
nonSubresourceReferralPathCondition(),
|
|
12574
|
-
|
|
12671
|
+
gte3(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12575
12672
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
12576
12673
|
)
|
|
12577
12674
|
).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
@@ -12587,7 +12684,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12587
12684
|
and12(
|
|
12588
12685
|
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12589
12686
|
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12590
|
-
|
|
12687
|
+
gte3(crawlerEventsHourly.tsHour, trendStart),
|
|
12591
12688
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12592
12689
|
)
|
|
12593
12690
|
).groupBy(sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
|
|
@@ -12598,7 +12695,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12598
12695
|
and12(
|
|
12599
12696
|
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12600
12697
|
nonSubresourceReferralPathCondition(),
|
|
12601
|
-
|
|
12698
|
+
gte3(aiReferralEventsHourly.tsHour, trendStart),
|
|
12602
12699
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
12603
12700
|
)
|
|
12604
12701
|
).groupBy(sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
|
|
@@ -12608,7 +12705,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12608
12705
|
}).from(aiUserFetchEventsHourly).where(
|
|
12609
12706
|
and12(
|
|
12610
12707
|
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12611
|
-
|
|
12708
|
+
gte3(aiUserFetchEventsHourly.tsHour, trendStart),
|
|
12612
12709
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
12613
12710
|
)
|
|
12614
12711
|
).groupBy(sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
|
|
@@ -14243,7 +14340,7 @@ async function visibilityStatsRoutes(app) {
|
|
|
14243
14340
|
}
|
|
14244
14341
|
|
|
14245
14342
|
// ../api-routes/src/results-export.ts
|
|
14246
|
-
import { and as and15, asc as
|
|
14343
|
+
import { and as and15, asc as asc4, eq as eq19, gte as gte4, lte as lte2 } from "drizzle-orm";
|
|
14247
14344
|
var CSV_COLUMNS = [
|
|
14248
14345
|
"export_schema_version",
|
|
14249
14346
|
"project_id",
|
|
@@ -14373,7 +14470,7 @@ async function resultsExportRoutes(app) {
|
|
|
14373
14470
|
eq19(runs.projectId, project.id),
|
|
14374
14471
|
eq19(runs.kind, RunKinds["answer-visibility"]),
|
|
14375
14472
|
...includeProbes ? [] : [notProbeRun()],
|
|
14376
|
-
...sinceIso ? [
|
|
14473
|
+
...sinceIso ? [gte4(runs.createdAt, sinceIso)] : [],
|
|
14377
14474
|
...untilIso ? [lte2(runs.createdAt, untilIso)] : []
|
|
14378
14475
|
];
|
|
14379
14476
|
const rows = app.db.select({
|
|
@@ -14398,7 +14495,7 @@ async function resultsExportRoutes(app) {
|
|
|
14398
14495
|
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
14399
14496
|
answerText: querySnapshots.answerText,
|
|
14400
14497
|
rawResponse: querySnapshots.rawResponse
|
|
14401
|
-
}).from(querySnapshots).innerJoin(runs, eq19(querySnapshots.runId, runs.id)).leftJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(and15(...conditions)).orderBy(
|
|
14498
|
+
}).from(querySnapshots).innerJoin(runs, eq19(querySnapshots.runId, runs.id)).leftJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(and15(...conditions)).orderBy(asc4(runs.createdAt), asc4(runs.id), asc4(querySnapshots.createdAt), asc4(querySnapshots.id)).all();
|
|
14402
14499
|
const records = rows.map((row) => {
|
|
14403
14500
|
const evidence = readEvidence(row.rawResponse);
|
|
14404
14501
|
const citationState = row.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"];
|
|
@@ -15166,6 +15263,7 @@ var SCHEMA_TABLE = {
|
|
|
15166
15263
|
AdsConnectionStatusDto: adsConnectionStatusDtoSchema,
|
|
15167
15264
|
AdsDisconnectResponse: adsDisconnectResponseSchema,
|
|
15168
15265
|
AdsInsightsResponse: adsInsightsResponseSchema,
|
|
15266
|
+
AdsOperationResponse: adsOperationResponseSchema,
|
|
15169
15267
|
AdsSummaryDto: adsSummaryDtoSchema,
|
|
15170
15268
|
AdsSyncResponse: adsSyncResponseSchema,
|
|
15171
15269
|
ApiKeyDto: apiKeyDtoSchema,
|
|
@@ -15234,6 +15332,7 @@ var SCHEMA_TABLE = {
|
|
|
15234
15332
|
GscSiteListResponseDto: gscSiteListResponseDtoSchema,
|
|
15235
15333
|
GscSitemapListResponseDto: gscSitemapListResponseDtoSchema,
|
|
15236
15334
|
GscUrlInspectionDto: gscUrlInspectionDtoSchema,
|
|
15335
|
+
HealthSnapshotDto: healthSnapshotDtoSchema,
|
|
15237
15336
|
IndexingRequestResponseDto: indexingRequestResponseDtoSchema,
|
|
15238
15337
|
KeywordDto: keywordDtoSchema,
|
|
15239
15338
|
LatestProjectRunDto: latestProjectRunDtoSchema,
|
|
@@ -15347,6 +15446,17 @@ var booleanSchema = { type: "boolean" };
|
|
|
15347
15446
|
var integerSchema = { type: "integer" };
|
|
15348
15447
|
var objectSchema = { type: "object", additionalProperties: true };
|
|
15349
15448
|
var stringArraySchema = { type: "array", items: stringSchema };
|
|
15449
|
+
var adsOperationKeySchema = { type: "string", minLength: 8, maxLength: 128 };
|
|
15450
|
+
var adsCreativeRequestSchema = {
|
|
15451
|
+
type: "object",
|
|
15452
|
+
required: ["title", "body", "targetUrl", "fileId"],
|
|
15453
|
+
properties: {
|
|
15454
|
+
title: { type: "string", minLength: 3, maxLength: 50 },
|
|
15455
|
+
body: { type: "string", minLength: 1, maxLength: 100 },
|
|
15456
|
+
targetUrl: { type: "string", format: "uri" },
|
|
15457
|
+
fileId: stringSchema
|
|
15458
|
+
}
|
|
15459
|
+
};
|
|
15350
15460
|
var googleConnectionTypeSchema = { type: "string", enum: ["gsc", "ga4", "gbp"] };
|
|
15351
15461
|
var locationSchema = {
|
|
15352
15462
|
type: "object",
|
|
@@ -16291,7 +16401,15 @@ var routeCatalog = [
|
|
|
16291
16401
|
path: "/api/v1/projects/{name}/history",
|
|
16292
16402
|
summary: "Get project audit history",
|
|
16293
16403
|
tags: ["history"],
|
|
16294
|
-
parameters: [
|
|
16404
|
+
parameters: [
|
|
16405
|
+
nameParameter,
|
|
16406
|
+
limitQueryParameter,
|
|
16407
|
+
offsetQueryParameter,
|
|
16408
|
+
{ name: "since", in: "query", description: "ISO 8601 lower bound.", schema: stringSchema },
|
|
16409
|
+
{ name: "action", in: "query", description: "Exact audit action filter.", schema: stringSchema },
|
|
16410
|
+
{ name: "actor", in: "query", description: "Exact actor filter.", schema: stringSchema },
|
|
16411
|
+
{ name: "entityType", in: "query", description: "Exact entity type filter.", schema: stringSchema }
|
|
16412
|
+
],
|
|
16295
16413
|
responses: {
|
|
16296
16414
|
200: jsonArrayResponse("Audit history returned.", "AuditLogEntry")
|
|
16297
16415
|
}
|
|
@@ -16301,6 +16419,14 @@ var routeCatalog = [
|
|
|
16301
16419
|
path: "/api/v1/history",
|
|
16302
16420
|
summary: "Get global audit history",
|
|
16303
16421
|
tags: ["history"],
|
|
16422
|
+
parameters: [
|
|
16423
|
+
limitQueryParameter,
|
|
16424
|
+
offsetQueryParameter,
|
|
16425
|
+
{ name: "since", in: "query", description: "ISO 8601 lower bound.", schema: stringSchema },
|
|
16426
|
+
{ name: "action", in: "query", description: "Exact audit action filter.", schema: stringSchema },
|
|
16427
|
+
{ name: "actor", in: "query", description: "Exact actor filter.", schema: stringSchema },
|
|
16428
|
+
{ name: "entityType", in: "query", description: "Exact entity type filter.", schema: stringSchema }
|
|
16429
|
+
],
|
|
16304
16430
|
responses: {
|
|
16305
16431
|
200: jsonArrayResponse("Audit history returned.", "AuditLogEntry")
|
|
16306
16432
|
}
|
|
@@ -16325,7 +16451,11 @@ var routeCatalog = [
|
|
|
16325
16451
|
path: "/api/v1/projects/{name}/timeline",
|
|
16326
16452
|
summary: "Get query timeline",
|
|
16327
16453
|
tags: ["history"],
|
|
16328
|
-
parameters: [
|
|
16454
|
+
parameters: [
|
|
16455
|
+
nameParameter,
|
|
16456
|
+
locationQueryParameter,
|
|
16457
|
+
{ name: "limit", in: "query", description: "Restrict each query timeline to snapshots from the most recent N project runs. Omit for full history.", schema: { ...integerSchema, minimum: 1, maximum: 100 } }
|
|
16458
|
+
],
|
|
16329
16459
|
responses: {
|
|
16330
16460
|
// TODO: Add `ProjectTimelineDto` Zod schema in contracts.
|
|
16331
16461
|
200: rawJsonResponse("Timeline returned.", looseObjectSchema)
|
|
@@ -17446,6 +17576,7 @@ var routeCatalog = [
|
|
|
17446
17576
|
responses: {
|
|
17447
17577
|
200: jsonResponse("Connected; key validated against the upstream ad account.", "AdsConnectionStatusDto"),
|
|
17448
17578
|
400: errorResponse("Missing/invalid key or credential storage unavailable."),
|
|
17579
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17449
17580
|
404: errorResponse("Project not found.")
|
|
17450
17581
|
}
|
|
17451
17582
|
},
|
|
@@ -17457,6 +17588,7 @@ var routeCatalog = [
|
|
|
17457
17588
|
parameters: [nameParameter],
|
|
17458
17589
|
responses: {
|
|
17459
17590
|
200: jsonResponse("Disconnected (idempotent).", "AdsDisconnectResponse"),
|
|
17591
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17460
17592
|
404: errorResponse("Project not found.")
|
|
17461
17593
|
}
|
|
17462
17594
|
},
|
|
@@ -17471,6 +17603,288 @@ var routeCatalog = [
|
|
|
17471
17603
|
404: errorResponse("Project not found.")
|
|
17472
17604
|
}
|
|
17473
17605
|
},
|
|
17606
|
+
{
|
|
17607
|
+
method: "get",
|
|
17608
|
+
path: "/api/v1/projects/{name}/ads/operations/{operationKey}",
|
|
17609
|
+
summary: "Read a durable OpenAI Ads mutation receipt by operation key",
|
|
17610
|
+
tags: ["ads"],
|
|
17611
|
+
parameters: [
|
|
17612
|
+
nameParameter,
|
|
17613
|
+
{ name: "operationKey", in: "path", required: true, description: "Caller-supplied idempotency key.", schema: adsOperationKeySchema }
|
|
17614
|
+
],
|
|
17615
|
+
responses: {
|
|
17616
|
+
200: jsonResponse("Operation receipt.", "AdsOperationResponse"),
|
|
17617
|
+
404: errorResponse("Project or operation not found.")
|
|
17618
|
+
}
|
|
17619
|
+
},
|
|
17620
|
+
{
|
|
17621
|
+
method: "post",
|
|
17622
|
+
path: "/api/v1/projects/{name}/ads/files",
|
|
17623
|
+
summary: "Upload an OpenAI Ads image from a public HTTPS URL",
|
|
17624
|
+
tags: ["ads"],
|
|
17625
|
+
parameters: [nameParameter],
|
|
17626
|
+
requestBody: {
|
|
17627
|
+
required: true,
|
|
17628
|
+
content: { "application/json": { schema: {
|
|
17629
|
+
type: "object",
|
|
17630
|
+
required: ["operationKey", "imageUrl"],
|
|
17631
|
+
properties: { operationKey: adsOperationKeySchema, imageUrl: { type: "string", format: "uri" } }
|
|
17632
|
+
} } }
|
|
17633
|
+
},
|
|
17634
|
+
responses: {
|
|
17635
|
+
200: jsonResponse("Upload receipt.", "AdsOperationResponse"),
|
|
17636
|
+
400: errorResponse("Invalid request or ads connection unavailable."),
|
|
17637
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17638
|
+
404: errorResponse("Project not found."),
|
|
17639
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17640
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17641
|
+
}
|
|
17642
|
+
},
|
|
17643
|
+
{
|
|
17644
|
+
method: "post",
|
|
17645
|
+
path: "/api/v1/projects/{name}/ads/campaigns",
|
|
17646
|
+
summary: "Create a paused OpenAI Ads campaign",
|
|
17647
|
+
description: "The server always creates the campaign paused. Status is not accepted from the caller.",
|
|
17648
|
+
tags: ["ads"],
|
|
17649
|
+
parameters: [nameParameter],
|
|
17650
|
+
requestBody: {
|
|
17651
|
+
required: true,
|
|
17652
|
+
content: { "application/json": { schema: {
|
|
17653
|
+
type: "object",
|
|
17654
|
+
required: ["operationKey", "name", "lifetimeSpendLimitMicros", "locationIds"],
|
|
17655
|
+
properties: {
|
|
17656
|
+
operationKey: adsOperationKeySchema,
|
|
17657
|
+
name: { type: "string", minLength: 3, maxLength: 1e3 },
|
|
17658
|
+
description: stringSchema,
|
|
17659
|
+
startTime: integerSchema,
|
|
17660
|
+
endTime: integerSchema,
|
|
17661
|
+
lifetimeSpendLimitMicros: { type: "integer", minimum: 1e6 },
|
|
17662
|
+
locationIds: { type: "array", minItems: 1, maxItems: 100, items: stringSchema }
|
|
17663
|
+
}
|
|
17664
|
+
} } }
|
|
17665
|
+
},
|
|
17666
|
+
responses: {
|
|
17667
|
+
200: jsonResponse("Paused campaign creation receipt.", "AdsOperationResponse"),
|
|
17668
|
+
400: errorResponse("Invalid request or ads connection unavailable."),
|
|
17669
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17670
|
+
404: errorResponse("Project not found."),
|
|
17671
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17672
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17673
|
+
}
|
|
17674
|
+
},
|
|
17675
|
+
{
|
|
17676
|
+
method: "post",
|
|
17677
|
+
path: "/api/v1/projects/{name}/ads/ad-groups",
|
|
17678
|
+
summary: "Create a paused OpenAI Ads ad group",
|
|
17679
|
+
description: "The server fixes billing to impressions and always creates the ad group paused.",
|
|
17680
|
+
tags: ["ads"],
|
|
17681
|
+
parameters: [nameParameter],
|
|
17682
|
+
requestBody: {
|
|
17683
|
+
required: true,
|
|
17684
|
+
content: { "application/json": { schema: {
|
|
17685
|
+
type: "object",
|
|
17686
|
+
required: ["operationKey", "campaignId", "name", "contextHints", "maxBidMicros"],
|
|
17687
|
+
properties: {
|
|
17688
|
+
operationKey: adsOperationKeySchema,
|
|
17689
|
+
campaignId: stringSchema,
|
|
17690
|
+
name: { type: "string", minLength: 3, maxLength: 1e3 },
|
|
17691
|
+
description: stringSchema,
|
|
17692
|
+
contextHints: { type: "array", minItems: 1, maxItems: 100, items: stringSchema },
|
|
17693
|
+
maxBidMicros: { type: "integer", minimum: 1, maximum: 1e8 }
|
|
17694
|
+
}
|
|
17695
|
+
} } }
|
|
17696
|
+
},
|
|
17697
|
+
responses: {
|
|
17698
|
+
200: jsonResponse("Paused ad-group creation receipt.", "AdsOperationResponse"),
|
|
17699
|
+
400: errorResponse("Invalid request or ads connection unavailable."),
|
|
17700
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17701
|
+
404: errorResponse("Project not found."),
|
|
17702
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17703
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17704
|
+
}
|
|
17705
|
+
},
|
|
17706
|
+
{
|
|
17707
|
+
method: "post",
|
|
17708
|
+
path: "/api/v1/projects/{name}/ads/ads",
|
|
17709
|
+
summary: "Create a paused OpenAI Ads chat-card ad",
|
|
17710
|
+
description: "The server always creates the ad paused and fixes creative.type to chat_card.",
|
|
17711
|
+
tags: ["ads"],
|
|
17712
|
+
parameters: [nameParameter],
|
|
17713
|
+
requestBody: {
|
|
17714
|
+
required: true,
|
|
17715
|
+
content: { "application/json": { schema: {
|
|
17716
|
+
type: "object",
|
|
17717
|
+
required: ["operationKey", "adGroupId", "name", "creative"],
|
|
17718
|
+
properties: {
|
|
17719
|
+
operationKey: adsOperationKeySchema,
|
|
17720
|
+
adGroupId: stringSchema,
|
|
17721
|
+
name: { type: "string", minLength: 3, maxLength: 1e3 },
|
|
17722
|
+
creative: adsCreativeRequestSchema
|
|
17723
|
+
}
|
|
17724
|
+
} } }
|
|
17725
|
+
},
|
|
17726
|
+
responses: {
|
|
17727
|
+
200: jsonResponse("Paused ad creation receipt.", "AdsOperationResponse"),
|
|
17728
|
+
400: errorResponse("Invalid request or ads connection unavailable."),
|
|
17729
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17730
|
+
404: errorResponse("Project not found."),
|
|
17731
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17732
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17733
|
+
}
|
|
17734
|
+
},
|
|
17735
|
+
{
|
|
17736
|
+
method: "post",
|
|
17737
|
+
path: "/api/v1/projects/{name}/ads/campaigns/{id}",
|
|
17738
|
+
summary: "Update an OpenAI Ads campaign with optimistic concurrency",
|
|
17739
|
+
description: "The campaign must already be paused. Pause and sync first, then pass the refreshed upstreamUpdatedAt. Activation remains human-only.",
|
|
17740
|
+
tags: ["ads"],
|
|
17741
|
+
parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Campaign ID.", schema: stringSchema }],
|
|
17742
|
+
requestBody: {
|
|
17743
|
+
required: true,
|
|
17744
|
+
content: { "application/json": { schema: {
|
|
17745
|
+
type: "object",
|
|
17746
|
+
required: ["operationKey", "expectedUpdatedAt"],
|
|
17747
|
+
properties: {
|
|
17748
|
+
operationKey: adsOperationKeySchema,
|
|
17749
|
+
expectedUpdatedAt: integerSchema,
|
|
17750
|
+
name: { type: "string", minLength: 3, maxLength: 1e3 },
|
|
17751
|
+
description: { type: "string", nullable: true },
|
|
17752
|
+
startTime: { type: "integer", nullable: true },
|
|
17753
|
+
endTime: { type: "integer", nullable: true },
|
|
17754
|
+
lifetimeSpendLimitMicros: { type: "integer", minimum: 1e6 },
|
|
17755
|
+
locationIds: { type: "array", minItems: 1, maxItems: 100, items: stringSchema }
|
|
17756
|
+
}
|
|
17757
|
+
} } }
|
|
17758
|
+
},
|
|
17759
|
+
responses: {
|
|
17760
|
+
200: jsonResponse("Campaign update receipt.", "AdsOperationResponse"),
|
|
17761
|
+
400: errorResponse("Invalid request, active entity, or stale expectedUpdatedAt."),
|
|
17762
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17763
|
+
404: errorResponse("Project not found."),
|
|
17764
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17765
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17766
|
+
}
|
|
17767
|
+
},
|
|
17768
|
+
{
|
|
17769
|
+
method: "post",
|
|
17770
|
+
path: "/api/v1/projects/{name}/ads/ad-groups/{id}",
|
|
17771
|
+
summary: "Update an OpenAI Ads ad group with optimistic concurrency",
|
|
17772
|
+
description: "The ad group must already be paused. Pause and sync first, then pass the refreshed upstreamUpdatedAt. Activation remains human-only.",
|
|
17773
|
+
tags: ["ads"],
|
|
17774
|
+
parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad group ID.", schema: stringSchema }],
|
|
17775
|
+
requestBody: {
|
|
17776
|
+
required: true,
|
|
17777
|
+
content: { "application/json": { schema: {
|
|
17778
|
+
type: "object",
|
|
17779
|
+
required: ["operationKey", "expectedUpdatedAt"],
|
|
17780
|
+
properties: {
|
|
17781
|
+
operationKey: adsOperationKeySchema,
|
|
17782
|
+
expectedUpdatedAt: integerSchema,
|
|
17783
|
+
name: { type: "string", minLength: 3, maxLength: 1e3 },
|
|
17784
|
+
description: { type: "string", nullable: true },
|
|
17785
|
+
contextHints: { type: "array", minItems: 1, maxItems: 100, items: stringSchema },
|
|
17786
|
+
maxBidMicros: { type: "integer", minimum: 1, maximum: 1e8 }
|
|
17787
|
+
}
|
|
17788
|
+
} } }
|
|
17789
|
+
},
|
|
17790
|
+
responses: {
|
|
17791
|
+
200: jsonResponse("Ad-group update receipt.", "AdsOperationResponse"),
|
|
17792
|
+
400: errorResponse("Invalid request, active entity, or stale expectedUpdatedAt."),
|
|
17793
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17794
|
+
404: errorResponse("Project not found."),
|
|
17795
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17796
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17797
|
+
}
|
|
17798
|
+
},
|
|
17799
|
+
{
|
|
17800
|
+
method: "post",
|
|
17801
|
+
path: "/api/v1/projects/{name}/ads/ads/{id}",
|
|
17802
|
+
summary: "Update an OpenAI Ads chat-card ad with optimistic concurrency",
|
|
17803
|
+
description: "The ad must already be paused. Pause and sync first, then pass the refreshed upstreamUpdatedAt. Activation remains human-only.",
|
|
17804
|
+
tags: ["ads"],
|
|
17805
|
+
parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad ID.", schema: stringSchema }],
|
|
17806
|
+
requestBody: {
|
|
17807
|
+
required: true,
|
|
17808
|
+
content: { "application/json": { schema: {
|
|
17809
|
+
type: "object",
|
|
17810
|
+
required: ["operationKey", "expectedUpdatedAt"],
|
|
17811
|
+
properties: {
|
|
17812
|
+
operationKey: adsOperationKeySchema,
|
|
17813
|
+
expectedUpdatedAt: integerSchema,
|
|
17814
|
+
name: { type: "string", minLength: 3, maxLength: 1e3 },
|
|
17815
|
+
creative: adsCreativeRequestSchema
|
|
17816
|
+
}
|
|
17817
|
+
} } }
|
|
17818
|
+
},
|
|
17819
|
+
responses: {
|
|
17820
|
+
200: jsonResponse("Ad update receipt.", "AdsOperationResponse"),
|
|
17821
|
+
400: errorResponse("Invalid request, active entity, or stale expectedUpdatedAt."),
|
|
17822
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17823
|
+
404: errorResponse("Project not found."),
|
|
17824
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17825
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17826
|
+
}
|
|
17827
|
+
},
|
|
17828
|
+
{
|
|
17829
|
+
method: "post",
|
|
17830
|
+
path: "/api/v1/projects/{name}/ads/campaigns/{id}/pause",
|
|
17831
|
+
summary: "Pause an OpenAI Ads campaign",
|
|
17832
|
+
tags: ["ads"],
|
|
17833
|
+
parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Campaign ID.", schema: stringSchema }],
|
|
17834
|
+
requestBody: { required: true, content: { "application/json": { schema: {
|
|
17835
|
+
type: "object",
|
|
17836
|
+
required: ["operationKey"],
|
|
17837
|
+
properties: { operationKey: adsOperationKeySchema }
|
|
17838
|
+
} } } },
|
|
17839
|
+
responses: {
|
|
17840
|
+
200: jsonResponse("Campaign pause receipt.", "AdsOperationResponse"),
|
|
17841
|
+
400: errorResponse("Invalid request or ads connection unavailable."),
|
|
17842
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17843
|
+
404: errorResponse("Project not found."),
|
|
17844
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17845
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17846
|
+
}
|
|
17847
|
+
},
|
|
17848
|
+
{
|
|
17849
|
+
method: "post",
|
|
17850
|
+
path: "/api/v1/projects/{name}/ads/ad-groups/{id}/pause",
|
|
17851
|
+
summary: "Pause an OpenAI Ads ad group",
|
|
17852
|
+
tags: ["ads"],
|
|
17853
|
+
parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad group ID.", schema: stringSchema }],
|
|
17854
|
+
requestBody: { required: true, content: { "application/json": { schema: {
|
|
17855
|
+
type: "object",
|
|
17856
|
+
required: ["operationKey"],
|
|
17857
|
+
properties: { operationKey: adsOperationKeySchema }
|
|
17858
|
+
} } } },
|
|
17859
|
+
responses: {
|
|
17860
|
+
200: jsonResponse("Ad-group pause receipt.", "AdsOperationResponse"),
|
|
17861
|
+
400: errorResponse("Invalid request or ads connection unavailable."),
|
|
17862
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17863
|
+
404: errorResponse("Project not found."),
|
|
17864
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17865
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17866
|
+
}
|
|
17867
|
+
},
|
|
17868
|
+
{
|
|
17869
|
+
method: "post",
|
|
17870
|
+
path: "/api/v1/projects/{name}/ads/ads/{id}/pause",
|
|
17871
|
+
summary: "Pause an OpenAI Ads ad",
|
|
17872
|
+
tags: ["ads"],
|
|
17873
|
+
parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad ID.", schema: stringSchema }],
|
|
17874
|
+
requestBody: { required: true, content: { "application/json": { schema: {
|
|
17875
|
+
type: "object",
|
|
17876
|
+
required: ["operationKey"],
|
|
17877
|
+
properties: { operationKey: adsOperationKeySchema }
|
|
17878
|
+
} } } },
|
|
17879
|
+
responses: {
|
|
17880
|
+
200: jsonResponse("Ad pause receipt.", "AdsOperationResponse"),
|
|
17881
|
+
400: errorResponse("Invalid request or ads connection unavailable."),
|
|
17882
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17883
|
+
404: errorResponse("Project not found."),
|
|
17884
|
+
409: errorResponse("Operation key was already used for a different request."),
|
|
17885
|
+
502: errorResponse("Upstream outcome failed or is unknown.")
|
|
17886
|
+
}
|
|
17887
|
+
},
|
|
17474
17888
|
{
|
|
17475
17889
|
method: "post",
|
|
17476
17890
|
path: "/api/v1/projects/{name}/ads/sync",
|
|
@@ -17480,13 +17894,14 @@ var routeCatalog = [
|
|
|
17480
17894
|
responses: {
|
|
17481
17895
|
200: jsonResponse("Sync run queued.", "AdsSyncResponse"),
|
|
17482
17896
|
400: errorResponse("No ads connection for this project."),
|
|
17897
|
+
403: errorResponse("The key lacks ads.write."),
|
|
17483
17898
|
404: errorResponse("Project not found.")
|
|
17484
17899
|
}
|
|
17485
17900
|
},
|
|
17486
17901
|
{
|
|
17487
17902
|
method: "get",
|
|
17488
17903
|
path: "/api/v1/projects/{name}/ads/campaigns",
|
|
17489
|
-
summary: "Synced campaign snapshots with nested ad groups
|
|
17904
|
+
summary: "Synced campaign snapshots with lifecycle timestamps, targeting, nested ad groups, and ads",
|
|
17490
17905
|
tags: ["ads"],
|
|
17491
17906
|
parameters: [nameParameter],
|
|
17492
17907
|
responses: {
|
|
@@ -18396,8 +18811,7 @@ var routeCatalog = [
|
|
|
18396
18811
|
tags: ["intelligence"],
|
|
18397
18812
|
parameters: [nameParameter],
|
|
18398
18813
|
responses: {
|
|
18399
|
-
|
|
18400
|
-
200: rawJsonResponse("Health snapshot or no-data sentinel returned.", looseObjectSchema),
|
|
18814
|
+
200: jsonResponse("Health snapshot or no-data sentinel returned.", "HealthSnapshotDto"),
|
|
18401
18815
|
404: errorResponse("Project not found.")
|
|
18402
18816
|
}
|
|
18403
18817
|
},
|
|
@@ -18408,11 +18822,10 @@ var routeCatalog = [
|
|
|
18408
18822
|
tags: ["intelligence"],
|
|
18409
18823
|
parameters: [
|
|
18410
18824
|
nameParameter,
|
|
18411
|
-
{ name: "limit", in: "query", description: "Max results.", schema:
|
|
18825
|
+
{ name: "limit", in: "query", description: "Max results.", schema: { ...integerSchema, minimum: 1, maximum: 100 } }
|
|
18412
18826
|
],
|
|
18413
18827
|
responses: {
|
|
18414
|
-
|
|
18415
|
-
200: rawJsonResponse("Health history returned.", { type: "array", items: looseObjectSchema }),
|
|
18828
|
+
200: jsonArrayResponse("Health history returned.", "HealthSnapshotDto"),
|
|
18416
18829
|
404: errorResponse("Project not found.")
|
|
18417
18830
|
}
|
|
18418
18831
|
},
|
|
@@ -19313,22 +19726,26 @@ var routeCatalog = [
|
|
|
19313
19726
|
method: "get",
|
|
19314
19727
|
path: "/api/v1/projects/{name}/technical-aeo",
|
|
19315
19728
|
summary: "Get the Technical AEO scorecard for a project",
|
|
19316
|
-
description: "Returns the latest completed/partial site-audit
|
|
19729
|
+
description: "Returns the latest completed/partial site-audit, or the historical audit selected by `runId`: aggregate 0\u2013100 score, page counts, the full per-factor scorecard (site-level averages with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the audit immediately before it. When the project has never been audited, `hasData` is false and the numeric fields are zeroed \u2014 render an onboarding state.",
|
|
19317
19730
|
tags: ["technical-aeo"],
|
|
19318
|
-
parameters: [
|
|
19731
|
+
parameters: [
|
|
19732
|
+
nameParameter,
|
|
19733
|
+
{ name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest audit.", schema: stringSchema }
|
|
19734
|
+
],
|
|
19319
19735
|
responses: {
|
|
19320
19736
|
200: jsonResponse("Technical AEO scorecard returned.", "SiteAuditScoreDto"),
|
|
19321
|
-
404: errorResponse("Project not found.")
|
|
19737
|
+
404: errorResponse("Project or site-audit run not found.")
|
|
19322
19738
|
}
|
|
19323
19739
|
},
|
|
19324
19740
|
{
|
|
19325
19741
|
method: "get",
|
|
19326
19742
|
path: "/api/v1/projects/{name}/technical-aeo/pages",
|
|
19327
|
-
summary: "List audited pages from
|
|
19328
|
-
description: "Returns the per-page breakdown of the latest completed/partial site-audit run (paginated). Filter to `status=error` to surface unreachable pages; sort `score-asc` (default) to surface the worst-scoring pages first.",
|
|
19743
|
+
summary: "List audited pages from a site-audit run",
|
|
19744
|
+
description: "Returns the per-page breakdown of the latest completed/partial site-audit run, or the historical audit selected by `runId` (paginated). Filter to `status=error` to surface unreachable pages; sort `score-asc` (default) to surface the worst-scoring pages first.",
|
|
19329
19745
|
tags: ["technical-aeo"],
|
|
19330
19746
|
parameters: [
|
|
19331
19747
|
nameParameter,
|
|
19748
|
+
{ name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest audit.", schema: stringSchema },
|
|
19332
19749
|
{ name: "status", in: "query", description: "Filter by page audit status: `success` or `error`.", schema: { type: "string", enum: ["success", "error"] } },
|
|
19333
19750
|
{ name: "sort", in: "query", description: "Sort order: `score-asc` (default), `score-desc`, or `url`.", schema: { type: "string", enum: ["score-asc", "score-desc", "url"] } },
|
|
19334
19751
|
limitQueryParameter,
|
|
@@ -19336,7 +19753,7 @@ var routeCatalog = [
|
|
|
19336
19753
|
],
|
|
19337
19754
|
responses: {
|
|
19338
19755
|
200: jsonResponse("Audited pages returned.", "SiteAuditPagesResponseDto"),
|
|
19339
|
-
404: errorResponse("Project not found.")
|
|
19756
|
+
404: errorResponse("Project or site-audit run not found.")
|
|
19340
19757
|
}
|
|
19341
19758
|
},
|
|
19342
19759
|
{
|
|
@@ -23078,7 +23495,17 @@ async function googleRoutes(app, opts) {
|
|
|
23078
23495
|
|
|
23079
23496
|
// ../api-routes/src/ads.ts
|
|
23080
23497
|
import crypto20 from "crypto";
|
|
23081
|
-
import { eq as eq25, and as and19, asc as
|
|
23498
|
+
import { eq as eq25, and as and19, asc as asc5, gte as gte5, lte as lte3, inArray as inArray12 } from "drizzle-orm";
|
|
23499
|
+
var AdsPausedPostconditionError = class extends Error {
|
|
23500
|
+
constructor(entityId) {
|
|
23501
|
+
super("OpenAI Ads API did not confirm the required paused state");
|
|
23502
|
+
this.entityId = entityId;
|
|
23503
|
+
this.name = "AdsPausedPostconditionError";
|
|
23504
|
+
}
|
|
23505
|
+
entityId;
|
|
23506
|
+
code = "ADS_PAUSED_POSTCONDITION_FAILED";
|
|
23507
|
+
status = 502;
|
|
23508
|
+
};
|
|
23082
23509
|
function statusDto(row) {
|
|
23083
23510
|
if (!row) return { connected: false };
|
|
23084
23511
|
return {
|
|
@@ -23099,15 +23526,185 @@ function creativeDto(raw) {
|
|
|
23099
23526
|
type: typeof c.type === "string" ? c.type : null,
|
|
23100
23527
|
title: typeof c.title === "string" ? c.title : null,
|
|
23101
23528
|
body: typeof c.body === "string" ? c.body : null,
|
|
23102
|
-
targetUrl: typeof c.target_url === "string" ? c.target_url : null
|
|
23529
|
+
targetUrl: typeof c.target_url === "string" ? c.target_url : null,
|
|
23530
|
+
fileId: typeof c.file_id === "string" ? c.file_id : null
|
|
23531
|
+
};
|
|
23532
|
+
}
|
|
23533
|
+
function locationIdsDto(raw) {
|
|
23534
|
+
if (!raw || typeof raw !== "object") return [];
|
|
23535
|
+
const locations = raw.locations;
|
|
23536
|
+
if (!locations || typeof locations !== "object") return [];
|
|
23537
|
+
const include = locations.include;
|
|
23538
|
+
if (!Array.isArray(include)) return [];
|
|
23539
|
+
return include.flatMap((entry) => {
|
|
23540
|
+
if (!entry || typeof entry !== "object") return [];
|
|
23541
|
+
const id = entry.id;
|
|
23542
|
+
return typeof id === "string" && id.length > 0 ? [id] : [];
|
|
23543
|
+
});
|
|
23544
|
+
}
|
|
23545
|
+
function operationDto(row) {
|
|
23546
|
+
return {
|
|
23547
|
+
id: row.id,
|
|
23548
|
+
operationKey: row.operationKey,
|
|
23549
|
+
kind: row.kind,
|
|
23550
|
+
state: row.state,
|
|
23551
|
+
entityType: row.entityType,
|
|
23552
|
+
entityId: row.entityId,
|
|
23553
|
+
upstreamUpdatedAt: row.upstreamUpdatedAt,
|
|
23554
|
+
errorCode: row.errorCode,
|
|
23555
|
+
errorMessage: row.errorMessage,
|
|
23556
|
+
createdAt: row.createdAt,
|
|
23557
|
+
updatedAt: row.updatedAt
|
|
23103
23558
|
};
|
|
23104
23559
|
}
|
|
23560
|
+
function canonicalize(value) {
|
|
23561
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
23562
|
+
if (!value || typeof value !== "object") return value;
|
|
23563
|
+
const out = {};
|
|
23564
|
+
for (const key of Object.keys(value).sort()) {
|
|
23565
|
+
const child = value[key];
|
|
23566
|
+
if (child !== void 0) out[key] = canonicalize(child);
|
|
23567
|
+
}
|
|
23568
|
+
return out;
|
|
23569
|
+
}
|
|
23570
|
+
function requestHash(value) {
|
|
23571
|
+
return crypto20.createHash("sha256").update(JSON.stringify(canonicalize(value))).digest("hex");
|
|
23572
|
+
}
|
|
23573
|
+
function errorDetails(err) {
|
|
23574
|
+
const candidate = err;
|
|
23575
|
+
const status = typeof candidate.status === "number" ? candidate.status : void 0;
|
|
23576
|
+
const knownClientFailure = err instanceof AppError || status !== void 0 && status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
23577
|
+
const rawCode = typeof candidate.code === "string" ? candidate.code : err instanceof AppError ? err.code : "upstream_error";
|
|
23578
|
+
const code = rawCode.replace(/[^\w.:-]/g, "_").slice(0, 100) || "upstream_error";
|
|
23579
|
+
const message = err instanceof AppError ? err.message.slice(0, 500) : knownClientFailure ? "OpenAI Ads API rejected the operation" : "OpenAI Ads API outcome could not be confirmed";
|
|
23580
|
+
return {
|
|
23581
|
+
state: knownClientFailure ? AdsOperationStates.failed : AdsOperationStates.unknown,
|
|
23582
|
+
code,
|
|
23583
|
+
message,
|
|
23584
|
+
entityId: typeof candidate.entityId === "string" ? candidate.entityId.slice(0, 200) : void 0
|
|
23585
|
+
};
|
|
23586
|
+
}
|
|
23587
|
+
function resolveAdsOperator(opts, projectName) {
|
|
23588
|
+
const apiKey = opts.adsCredentialStore?.getConnection(projectName)?.apiKey;
|
|
23589
|
+
if (!apiKey) {
|
|
23590
|
+
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
23591
|
+
}
|
|
23592
|
+
if (!opts.adsOperator) {
|
|
23593
|
+
throw validationError("Ads lifecycle operations are not configured for this deployment");
|
|
23594
|
+
}
|
|
23595
|
+
return { apiKey, operator: opts.adsOperator };
|
|
23596
|
+
}
|
|
23597
|
+
async function executeAdsOperation(app, request, input) {
|
|
23598
|
+
const hash = requestHash({ kind: input.kind, entityType: input.entityType, payload: input.payload });
|
|
23599
|
+
const existing = app.db.select().from(adsOperations).where(and19(
|
|
23600
|
+
eq25(adsOperations.projectId, input.projectId),
|
|
23601
|
+
eq25(adsOperations.operationKey, input.operationKey)
|
|
23602
|
+
)).get();
|
|
23603
|
+
if (existing) {
|
|
23604
|
+
if (existing.requestHash !== hash) {
|
|
23605
|
+
throw alreadyExists("Ads operation key", input.operationKey);
|
|
23606
|
+
}
|
|
23607
|
+
return { operation: operationDto(existing), replayed: true };
|
|
23608
|
+
}
|
|
23609
|
+
const id = crypto20.randomUUID();
|
|
23610
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23611
|
+
app.db.insert(adsOperations).values({
|
|
23612
|
+
id,
|
|
23613
|
+
projectId: input.projectId,
|
|
23614
|
+
operationKey: input.operationKey,
|
|
23615
|
+
requestHash: hash,
|
|
23616
|
+
kind: input.kind,
|
|
23617
|
+
state: AdsOperationStates.pending,
|
|
23618
|
+
entityType: input.entityType,
|
|
23619
|
+
createdAt,
|
|
23620
|
+
updatedAt: createdAt
|
|
23621
|
+
}).run();
|
|
23622
|
+
let result;
|
|
23623
|
+
try {
|
|
23624
|
+
result = await input.run();
|
|
23625
|
+
if (input.expectedStatus !== void 0 && result.status !== input.expectedStatus) {
|
|
23626
|
+
if (input.remediateStatus) result = await input.remediateStatus(result);
|
|
23627
|
+
if (result.status !== input.expectedStatus) {
|
|
23628
|
+
throw new AdsPausedPostconditionError(result.id);
|
|
23629
|
+
}
|
|
23630
|
+
}
|
|
23631
|
+
const succeededResult = result;
|
|
23632
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23633
|
+
app.db.transaction((tx) => {
|
|
23634
|
+
tx.update(adsOperations).set({
|
|
23635
|
+
state: AdsOperationStates.succeeded,
|
|
23636
|
+
entityId: succeededResult.id,
|
|
23637
|
+
upstreamUpdatedAt: succeededResult.updatedAt ?? null,
|
|
23638
|
+
updatedAt
|
|
23639
|
+
}).where(eq25(adsOperations.id, id)).run();
|
|
23640
|
+
writeAuditLog(tx, auditFromRequest(request, {
|
|
23641
|
+
projectId: input.projectId,
|
|
23642
|
+
actor: "api",
|
|
23643
|
+
action: `ads.${input.kind}.succeeded`,
|
|
23644
|
+
entityType: input.entityType,
|
|
23645
|
+
entityId: succeededResult.id,
|
|
23646
|
+
diff: { operationId: id, operationKey: input.operationKey }
|
|
23647
|
+
}));
|
|
23648
|
+
});
|
|
23649
|
+
const row = app.db.select().from(adsOperations).where(eq25(adsOperations.id, id)).get();
|
|
23650
|
+
return { operation: operationDto(row), replayed: false };
|
|
23651
|
+
} catch (err) {
|
|
23652
|
+
const failure = errorDetails(err);
|
|
23653
|
+
const entityId = failure.entityId ?? result?.id;
|
|
23654
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23655
|
+
app.db.transaction((tx) => {
|
|
23656
|
+
tx.update(adsOperations).set({
|
|
23657
|
+
state: failure.state,
|
|
23658
|
+
entityId,
|
|
23659
|
+
errorCode: failure.code,
|
|
23660
|
+
errorMessage: failure.message,
|
|
23661
|
+
updatedAt
|
|
23662
|
+
}).where(eq25(adsOperations.id, id)).run();
|
|
23663
|
+
writeAuditLog(tx, auditFromRequest(request, {
|
|
23664
|
+
projectId: input.projectId,
|
|
23665
|
+
actor: "api",
|
|
23666
|
+
action: `ads.${input.kind}.${failure.state}`,
|
|
23667
|
+
entityType: input.entityType,
|
|
23668
|
+
entityId: entityId ?? id,
|
|
23669
|
+
diff: { operationId: id, operationKey: input.operationKey, errorCode: failure.code }
|
|
23670
|
+
}));
|
|
23671
|
+
});
|
|
23672
|
+
if (err instanceof AppError) throw err;
|
|
23673
|
+
throw providerError("OpenAI Ads API mutation failed", {
|
|
23674
|
+
operationId: id,
|
|
23675
|
+
operationKey: input.operationKey,
|
|
23676
|
+
state: failure.state,
|
|
23677
|
+
code: failure.code
|
|
23678
|
+
});
|
|
23679
|
+
}
|
|
23680
|
+
}
|
|
23681
|
+
function parseBody(schema, value) {
|
|
23682
|
+
const parsed = schema.safeParse(value);
|
|
23683
|
+
if (!parsed.success) throw validationError("Invalid ads operation request", { issues: parsed.error.issues });
|
|
23684
|
+
return parsed.data;
|
|
23685
|
+
}
|
|
23686
|
+
function assertExpectedUpdatedAt(entity, expected) {
|
|
23687
|
+
if (entity.updatedAt !== expected) {
|
|
23688
|
+
throw validationError("The upstream ad entity changed since it was reviewed", {
|
|
23689
|
+
expectedUpdatedAt: expected,
|
|
23690
|
+
actualUpdatedAt: entity.updatedAt
|
|
23691
|
+
});
|
|
23692
|
+
}
|
|
23693
|
+
}
|
|
23694
|
+
function assertPausedForUpdate(entity) {
|
|
23695
|
+
if (entity.status !== AdsEntityStatuses.paused) {
|
|
23696
|
+
throw validationError("Pause the upstream ad entity before updating it", {
|
|
23697
|
+
actualStatus: entity.status
|
|
23698
|
+
});
|
|
23699
|
+
}
|
|
23700
|
+
}
|
|
23105
23701
|
async function adsRoutes(app, opts) {
|
|
23106
23702
|
app.post(
|
|
23107
23703
|
"/projects/:name/ads/connect",
|
|
23108
23704
|
async (request) => {
|
|
23705
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23109
23706
|
const project = resolveProject(app.db, request.params.name);
|
|
23110
|
-
const parsed = adsConnectRequestSchema.safeParse(request.body
|
|
23707
|
+
const parsed = adsConnectRequestSchema.safeParse(request.body);
|
|
23111
23708
|
if (!parsed.success) throw validationError('"apiKey" is required');
|
|
23112
23709
|
if (!opts.adsCredentialStore || !opts.verifyAdsAccount) {
|
|
23113
23710
|
throw validationError("Ads credential storage is not configured for this deployment");
|
|
@@ -23165,6 +23762,7 @@ async function adsRoutes(app, opts) {
|
|
|
23165
23762
|
}
|
|
23166
23763
|
);
|
|
23167
23764
|
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
23765
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23168
23766
|
const project = resolveProject(app.db, request.params.name);
|
|
23169
23767
|
const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
23170
23768
|
if (row) {
|
|
@@ -23188,7 +23786,221 @@ async function adsRoutes(app, opts) {
|
|
|
23188
23786
|
const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
23189
23787
|
return statusDto(row);
|
|
23190
23788
|
});
|
|
23789
|
+
app.get(
|
|
23790
|
+
"/projects/:name/ads/operations/:operationKey",
|
|
23791
|
+
async (request) => {
|
|
23792
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23793
|
+
const row = app.db.select().from(adsOperations).where(and19(
|
|
23794
|
+
eq25(adsOperations.projectId, project.id),
|
|
23795
|
+
eq25(adsOperations.operationKey, request.params.operationKey)
|
|
23796
|
+
)).get();
|
|
23797
|
+
if (!row) throw notFound("Ads operation", request.params.operationKey);
|
|
23798
|
+
const response = { operation: operationDto(row), replayed: true };
|
|
23799
|
+
return response;
|
|
23800
|
+
}
|
|
23801
|
+
);
|
|
23802
|
+
app.post("/projects/:name/ads/files", async (request) => {
|
|
23803
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23804
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23805
|
+
const body = parseBody(adsImageUploadRequestSchema, request.body);
|
|
23806
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23807
|
+
return executeAdsOperation(app, request, {
|
|
23808
|
+
projectId: project.id,
|
|
23809
|
+
operationKey: body.operationKey,
|
|
23810
|
+
kind: AdsOperationKinds.image_upload,
|
|
23811
|
+
entityType: AdsEntityTypes.file,
|
|
23812
|
+
payload: { imageUrl: body.imageUrl },
|
|
23813
|
+
run: async () => {
|
|
23814
|
+
const result = await operator.uploadImage(apiKey, body.imageUrl);
|
|
23815
|
+
return { id: result.fileId };
|
|
23816
|
+
}
|
|
23817
|
+
});
|
|
23818
|
+
});
|
|
23819
|
+
app.post("/projects/:name/ads/campaigns", async (request) => {
|
|
23820
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23821
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23822
|
+
const body = parseBody(adsCampaignCreateRequestSchema, request.body);
|
|
23823
|
+
const { operationKey, ...providerInput } = body;
|
|
23824
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23825
|
+
return executeAdsOperation(app, request, {
|
|
23826
|
+
projectId: project.id,
|
|
23827
|
+
operationKey,
|
|
23828
|
+
kind: AdsOperationKinds.campaign_create,
|
|
23829
|
+
entityType: AdsEntityTypes.campaign,
|
|
23830
|
+
payload: providerInput,
|
|
23831
|
+
expectedStatus: AdsEntityStatuses.paused,
|
|
23832
|
+
remediateStatus: (result) => operator.pauseCampaign(apiKey, result.id),
|
|
23833
|
+
run: async () => operator.createCampaign(apiKey, providerInput)
|
|
23834
|
+
});
|
|
23835
|
+
});
|
|
23836
|
+
app.post("/projects/:name/ads/ad-groups", async (request) => {
|
|
23837
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23838
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23839
|
+
const body = parseBody(adsAdGroupCreateRequestSchema, request.body);
|
|
23840
|
+
const { operationKey, ...providerInput } = body;
|
|
23841
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23842
|
+
return executeAdsOperation(app, request, {
|
|
23843
|
+
projectId: project.id,
|
|
23844
|
+
operationKey,
|
|
23845
|
+
kind: AdsOperationKinds.ad_group_create,
|
|
23846
|
+
entityType: AdsEntityTypes.ad_group,
|
|
23847
|
+
payload: providerInput,
|
|
23848
|
+
expectedStatus: AdsEntityStatuses.paused,
|
|
23849
|
+
remediateStatus: (result) => operator.pauseAdGroup(apiKey, result.id),
|
|
23850
|
+
run: async () => operator.createAdGroup(apiKey, providerInput)
|
|
23851
|
+
});
|
|
23852
|
+
});
|
|
23853
|
+
app.post("/projects/:name/ads/ads", async (request) => {
|
|
23854
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23855
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23856
|
+
const body = parseBody(adsAdCreateRequestSchema, request.body);
|
|
23857
|
+
const { operationKey, ...providerInput } = body;
|
|
23858
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23859
|
+
return executeAdsOperation(app, request, {
|
|
23860
|
+
projectId: project.id,
|
|
23861
|
+
operationKey,
|
|
23862
|
+
kind: AdsOperationKinds.ad_create,
|
|
23863
|
+
entityType: AdsEntityTypes.ad,
|
|
23864
|
+
payload: providerInput,
|
|
23865
|
+
expectedStatus: AdsEntityStatuses.paused,
|
|
23866
|
+
remediateStatus: (result) => operator.pauseAd(apiKey, result.id),
|
|
23867
|
+
run: async () => operator.createAd(apiKey, providerInput)
|
|
23868
|
+
});
|
|
23869
|
+
});
|
|
23870
|
+
app.post(
|
|
23871
|
+
"/projects/:name/ads/campaigns/:id",
|
|
23872
|
+
async (request) => {
|
|
23873
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23874
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23875
|
+
const body = parseBody(adsCampaignUpdateRequestSchema, request.body);
|
|
23876
|
+
const { operationKey, expectedUpdatedAt, ...update } = body;
|
|
23877
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23878
|
+
return executeAdsOperation(app, request, {
|
|
23879
|
+
projectId: project.id,
|
|
23880
|
+
operationKey,
|
|
23881
|
+
kind: AdsOperationKinds.campaign_update,
|
|
23882
|
+
entityType: AdsEntityTypes.campaign,
|
|
23883
|
+
payload: { id: request.params.id, expectedUpdatedAt, update },
|
|
23884
|
+
expectedStatus: AdsEntityStatuses.paused,
|
|
23885
|
+
remediateStatus: (result) => operator.pauseCampaign(apiKey, result.id),
|
|
23886
|
+
run: async () => {
|
|
23887
|
+
const current = await operator.getCampaign(apiKey, request.params.id);
|
|
23888
|
+
assertExpectedUpdatedAt(current, expectedUpdatedAt);
|
|
23889
|
+
assertPausedForUpdate(current);
|
|
23890
|
+
return operator.updateCampaign(apiKey, request.params.id, update);
|
|
23891
|
+
}
|
|
23892
|
+
});
|
|
23893
|
+
}
|
|
23894
|
+
);
|
|
23895
|
+
app.post(
|
|
23896
|
+
"/projects/:name/ads/ad-groups/:id",
|
|
23897
|
+
async (request) => {
|
|
23898
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23899
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23900
|
+
const body = parseBody(adsAdGroupUpdateRequestSchema, request.body);
|
|
23901
|
+
const { operationKey, expectedUpdatedAt, ...update } = body;
|
|
23902
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23903
|
+
return executeAdsOperation(app, request, {
|
|
23904
|
+
projectId: project.id,
|
|
23905
|
+
operationKey,
|
|
23906
|
+
kind: AdsOperationKinds.ad_group_update,
|
|
23907
|
+
entityType: AdsEntityTypes.ad_group,
|
|
23908
|
+
payload: { id: request.params.id, expectedUpdatedAt, update },
|
|
23909
|
+
expectedStatus: AdsEntityStatuses.paused,
|
|
23910
|
+
remediateStatus: (result) => operator.pauseAdGroup(apiKey, result.id),
|
|
23911
|
+
run: async () => {
|
|
23912
|
+
const current = await operator.getAdGroup(apiKey, request.params.id);
|
|
23913
|
+
assertExpectedUpdatedAt(current, expectedUpdatedAt);
|
|
23914
|
+
assertPausedForUpdate(current);
|
|
23915
|
+
return operator.updateAdGroup(apiKey, request.params.id, update);
|
|
23916
|
+
}
|
|
23917
|
+
});
|
|
23918
|
+
}
|
|
23919
|
+
);
|
|
23920
|
+
app.post(
|
|
23921
|
+
"/projects/:name/ads/ads/:id",
|
|
23922
|
+
async (request) => {
|
|
23923
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23924
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23925
|
+
const body = parseBody(adsAdUpdateRequestSchema, request.body);
|
|
23926
|
+
const { operationKey, expectedUpdatedAt, ...update } = body;
|
|
23927
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23928
|
+
return executeAdsOperation(app, request, {
|
|
23929
|
+
projectId: project.id,
|
|
23930
|
+
operationKey,
|
|
23931
|
+
kind: AdsOperationKinds.ad_update,
|
|
23932
|
+
entityType: AdsEntityTypes.ad,
|
|
23933
|
+
payload: { id: request.params.id, expectedUpdatedAt, update },
|
|
23934
|
+
expectedStatus: AdsEntityStatuses.paused,
|
|
23935
|
+
remediateStatus: (result) => operator.pauseAd(apiKey, result.id),
|
|
23936
|
+
run: async () => {
|
|
23937
|
+
const current = await operator.getAd(apiKey, request.params.id);
|
|
23938
|
+
assertExpectedUpdatedAt(current, expectedUpdatedAt);
|
|
23939
|
+
assertPausedForUpdate(current);
|
|
23940
|
+
return operator.updateAd(apiKey, request.params.id, update);
|
|
23941
|
+
}
|
|
23942
|
+
});
|
|
23943
|
+
}
|
|
23944
|
+
);
|
|
23945
|
+
app.post(
|
|
23946
|
+
"/projects/:name/ads/campaigns/:id/pause",
|
|
23947
|
+
async (request) => {
|
|
23948
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23949
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23950
|
+
const body = parseBody(adsPauseRequestSchema, request.body);
|
|
23951
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23952
|
+
return executeAdsOperation(app, request, {
|
|
23953
|
+
projectId: project.id,
|
|
23954
|
+
operationKey: body.operationKey,
|
|
23955
|
+
kind: AdsOperationKinds.campaign_pause,
|
|
23956
|
+
entityType: AdsEntityTypes.campaign,
|
|
23957
|
+
payload: { id: request.params.id },
|
|
23958
|
+
expectedStatus: AdsEntityStatuses.paused,
|
|
23959
|
+
remediateStatus: (result) => operator.pauseCampaign(apiKey, result.id),
|
|
23960
|
+
run: async () => operator.pauseCampaign(apiKey, request.params.id)
|
|
23961
|
+
});
|
|
23962
|
+
}
|
|
23963
|
+
);
|
|
23964
|
+
app.post(
|
|
23965
|
+
"/projects/:name/ads/ad-groups/:id/pause",
|
|
23966
|
+
async (request) => {
|
|
23967
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23968
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23969
|
+
const body = parseBody(adsPauseRequestSchema, request.body);
|
|
23970
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23971
|
+
return executeAdsOperation(app, request, {
|
|
23972
|
+
projectId: project.id,
|
|
23973
|
+
operationKey: body.operationKey,
|
|
23974
|
+
kind: AdsOperationKinds.ad_group_pause,
|
|
23975
|
+
entityType: AdsEntityTypes.ad_group,
|
|
23976
|
+
payload: { id: request.params.id },
|
|
23977
|
+
expectedStatus: AdsEntityStatuses.paused,
|
|
23978
|
+
remediateStatus: (result) => operator.pauseAdGroup(apiKey, result.id),
|
|
23979
|
+
run: async () => operator.pauseAdGroup(apiKey, request.params.id)
|
|
23980
|
+
});
|
|
23981
|
+
}
|
|
23982
|
+
);
|
|
23983
|
+
app.post(
|
|
23984
|
+
"/projects/:name/ads/ads/:id/pause",
|
|
23985
|
+
async (request) => {
|
|
23986
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23987
|
+
const project = resolveProject(app.db, request.params.name);
|
|
23988
|
+
const body = parseBody(adsPauseRequestSchema, request.body);
|
|
23989
|
+
const { apiKey, operator } = resolveAdsOperator(opts, project.name);
|
|
23990
|
+
return executeAdsOperation(app, request, {
|
|
23991
|
+
projectId: project.id,
|
|
23992
|
+
operationKey: body.operationKey,
|
|
23993
|
+
kind: AdsOperationKinds.ad_pause,
|
|
23994
|
+
entityType: AdsEntityTypes.ad,
|
|
23995
|
+
payload: { id: request.params.id },
|
|
23996
|
+
expectedStatus: AdsEntityStatuses.paused,
|
|
23997
|
+
remediateStatus: (result) => operator.pauseAd(apiKey, result.id),
|
|
23998
|
+
run: async () => operator.pauseAd(apiKey, request.params.id)
|
|
23999
|
+
});
|
|
24000
|
+
}
|
|
24001
|
+
);
|
|
23191
24002
|
app.post("/projects/:name/ads/sync", async (request) => {
|
|
24003
|
+
requireScope(request, ADS_WRITE_SCOPE);
|
|
23192
24004
|
const project = resolveProject(app.db, request.params.name);
|
|
23193
24005
|
const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
|
|
23194
24006
|
if (!row) {
|
|
@@ -23229,7 +24041,9 @@ async function adsRoutes(app, opts) {
|
|
|
23229
24041
|
name: ad.name,
|
|
23230
24042
|
status: ad.status,
|
|
23231
24043
|
reviewStatus: ad.reviewStatus,
|
|
23232
|
-
creative: creativeDto(ad.creative)
|
|
24044
|
+
creative: creativeDto(ad.creative),
|
|
24045
|
+
upstreamUpdatedAt: ad.upstreamUpdatedAt,
|
|
24046
|
+
syncedAt: ad.syncedAt
|
|
23233
24047
|
};
|
|
23234
24048
|
const list = adsByGroup.get(ad.adGroupId) ?? [];
|
|
23235
24049
|
list.push(dto);
|
|
@@ -23241,11 +24055,14 @@ async function adsRoutes(app, opts) {
|
|
|
23241
24055
|
id: group.id,
|
|
23242
24056
|
campaignId: group.campaignId,
|
|
23243
24057
|
name: group.name,
|
|
24058
|
+
description: group.description,
|
|
23244
24059
|
status: group.status,
|
|
23245
24060
|
billingEventType: group.billingEventType,
|
|
23246
24061
|
maxBidMicros: group.maxBidMicros,
|
|
23247
24062
|
contextHints: group.contextHints,
|
|
23248
|
-
ads: adsByGroup.get(group.id) ?? []
|
|
24063
|
+
ads: adsByGroup.get(group.id) ?? [],
|
|
24064
|
+
upstreamUpdatedAt: group.upstreamUpdatedAt,
|
|
24065
|
+
syncedAt: group.syncedAt
|
|
23249
24066
|
};
|
|
23250
24067
|
const list = groupsByCampaign.get(group.campaignId) ?? [];
|
|
23251
24068
|
list.push(dto);
|
|
@@ -23254,11 +24071,17 @@ async function adsRoutes(app, opts) {
|
|
|
23254
24071
|
const campaigns = campaignRows.map((campaign) => ({
|
|
23255
24072
|
id: campaign.id,
|
|
23256
24073
|
name: campaign.name,
|
|
24074
|
+
description: campaign.description,
|
|
23257
24075
|
status: campaign.status,
|
|
24076
|
+
startTime: campaign.startTime,
|
|
24077
|
+
endTime: campaign.endTime,
|
|
23258
24078
|
biddingType: campaign.biddingType,
|
|
23259
24079
|
dailySpendLimitMicros: campaign.dailySpendLimitMicros,
|
|
23260
24080
|
lifetimeSpendLimitMicros: campaign.lifetimeSpendLimitMicros,
|
|
23261
|
-
|
|
24081
|
+
locationIds: locationIdsDto(campaign.targeting),
|
|
24082
|
+
adGroups: groupsByCampaign.get(campaign.id) ?? [],
|
|
24083
|
+
upstreamUpdatedAt: campaign.upstreamUpdatedAt,
|
|
24084
|
+
syncedAt: campaign.syncedAt
|
|
23262
24085
|
}));
|
|
23263
24086
|
const response = { campaigns };
|
|
23264
24087
|
return response;
|
|
@@ -23277,9 +24100,9 @@ async function adsRoutes(app, opts) {
|
|
|
23277
24100
|
const conditions = [eq25(adsInsightsDaily.projectId, project.id)];
|
|
23278
24101
|
if (parsedLevel) conditions.push(eq25(adsInsightsDaily.level, parsedLevel));
|
|
23279
24102
|
if (entityId) conditions.push(eq25(adsInsightsDaily.entityId, entityId));
|
|
23280
|
-
if (from) conditions.push(
|
|
24103
|
+
if (from) conditions.push(gte5(adsInsightsDaily.date, from));
|
|
23281
24104
|
if (to) conditions.push(lte3(adsInsightsDaily.date, to));
|
|
23282
|
-
const rows = app.db.select().from(adsInsightsDaily).where(and19(...conditions)).orderBy(
|
|
24105
|
+
const rows = app.db.select().from(adsInsightsDaily).where(and19(...conditions)).orderBy(asc5(adsInsightsDaily.date)).all();
|
|
23283
24106
|
const dtoRows = rows.map((row) => ({
|
|
23284
24107
|
level: row.level,
|
|
23285
24108
|
entityId: row.entityId,
|
|
@@ -26768,7 +27591,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
26768
27591
|
|
|
26769
27592
|
// ../api-routes/src/backlinks.ts
|
|
26770
27593
|
import crypto24 from "crypto";
|
|
26771
|
-
import { and as and24, asc as
|
|
27594
|
+
import { and as and24, asc as asc6, desc as desc14, eq as eq29, sql as sql11 } from "drizzle-orm";
|
|
26772
27595
|
|
|
26773
27596
|
// ../integration-commoncrawl/src/constants.ts
|
|
26774
27597
|
import os2 from "os";
|
|
@@ -27527,7 +28350,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27527
28350
|
async (request, reply) => {
|
|
27528
28351
|
const project = resolveProject(app.db, request.params.name);
|
|
27529
28352
|
const source = parseSourceParam(request.query.source);
|
|
27530
|
-
const rows = app.db.select().from(backlinkSummaries).where(and24(eq29(backlinkSummaries.projectId, project.id), eq29(backlinkSummaries.source, source))).orderBy(
|
|
28353
|
+
const rows = app.db.select().from(backlinkSummaries).where(and24(eq29(backlinkSummaries.projectId, project.id), eq29(backlinkSummaries.source, source))).orderBy(asc6(backlinkSummaries.queriedAt)).all();
|
|
27531
28354
|
const response = rows.map((r) => ({
|
|
27532
28355
|
release: r.release,
|
|
27533
28356
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -27583,7 +28406,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
27583
28406
|
// ../api-routes/src/traffic.ts
|
|
27584
28407
|
import crypto26 from "crypto";
|
|
27585
28408
|
import { Agent as UndiciAgent } from "undici";
|
|
27586
|
-
import { and as and25, desc as desc15, eq as eq30, gte as
|
|
28409
|
+
import { and as and25, desc as desc15, eq as eq30, gte as gte6, lte as lte4, sql as sql12 } from "drizzle-orm";
|
|
27587
28410
|
|
|
27588
28411
|
// ../integration-cloud-run/src/auth.ts
|
|
27589
28412
|
import crypto25 from "crypto";
|
|
@@ -31490,28 +32313,28 @@ async function runBackfillTask(options) {
|
|
|
31490
32313
|
tx.delete(crawlerEventsHourly).where(
|
|
31491
32314
|
and25(
|
|
31492
32315
|
eq30(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
31493
|
-
|
|
32316
|
+
gte6(crawlerEventsHourly.tsHour, windowStartIso),
|
|
31494
32317
|
lte4(crawlerEventsHourly.tsHour, windowEndIso)
|
|
31495
32318
|
)
|
|
31496
32319
|
).run();
|
|
31497
32320
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
31498
32321
|
and25(
|
|
31499
32322
|
eq30(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
31500
|
-
|
|
32323
|
+
gte6(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
31501
32324
|
lte4(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
31502
32325
|
)
|
|
31503
32326
|
).run();
|
|
31504
32327
|
tx.delete(aiReferralEventsHourly).where(
|
|
31505
32328
|
and25(
|
|
31506
32329
|
eq30(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
31507
|
-
|
|
32330
|
+
gte6(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
31508
32331
|
lte4(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
31509
32332
|
)
|
|
31510
32333
|
).run();
|
|
31511
32334
|
tx.delete(rawEventSamples).where(
|
|
31512
32335
|
and25(
|
|
31513
32336
|
eq30(rawEventSamples.sourceId, sourceRow.id),
|
|
31514
|
-
|
|
32337
|
+
gte6(rawEventSamples.ts, windowStartIso),
|
|
31515
32338
|
lte4(rawEventSamples.ts, windowEndIso)
|
|
31516
32339
|
)
|
|
31517
32340
|
).run();
|
|
@@ -32625,7 +33448,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32625
33448
|
}).from(crawlerEventsHourly).where(
|
|
32626
33449
|
and25(
|
|
32627
33450
|
eq30(crawlerEventsHourly.sourceId, row.id),
|
|
32628
|
-
|
|
33451
|
+
gte6(crawlerEventsHourly.tsHour, since)
|
|
32629
33452
|
)
|
|
32630
33453
|
).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
32631
33454
|
const crawlerSegments = segmentCrawlerHits(
|
|
@@ -32635,19 +33458,19 @@ async function trafficRoutes(app, opts) {
|
|
|
32635
33458
|
const aiUserFetchTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
32636
33459
|
and25(
|
|
32637
33460
|
eq30(aiUserFetchEventsHourly.sourceId, row.id),
|
|
32638
|
-
|
|
33461
|
+
gte6(aiUserFetchEventsHourly.tsHour, since)
|
|
32639
33462
|
)
|
|
32640
33463
|
).get();
|
|
32641
33464
|
const aiTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
32642
33465
|
and25(
|
|
32643
33466
|
eq30(aiReferralEventsHourly.sourceId, row.id),
|
|
32644
|
-
|
|
33467
|
+
gte6(aiReferralEventsHourly.tsHour, since)
|
|
32645
33468
|
)
|
|
32646
33469
|
).get();
|
|
32647
33470
|
const sampleTotals = app.db.select({ total: sql12`COUNT(*)` }).from(rawEventSamples).where(
|
|
32648
33471
|
and25(
|
|
32649
33472
|
eq30(rawEventSamples.sourceId, row.id),
|
|
32650
|
-
|
|
33473
|
+
gte6(rawEventSamples.ts, since)
|
|
32651
33474
|
)
|
|
32652
33475
|
).get();
|
|
32653
33476
|
const latestRun = app.db.select().from(runs).where(
|
|
@@ -32786,7 +33609,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32786
33609
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
32787
33610
|
const crawlerFilters = [
|
|
32788
33611
|
eq30(crawlerEventsHourly.projectId, project.id),
|
|
32789
|
-
|
|
33612
|
+
gte6(crawlerEventsHourly.tsHour, sinceIso),
|
|
32790
33613
|
lte4(crawlerEventsHourly.tsHour, untilIso)
|
|
32791
33614
|
];
|
|
32792
33615
|
if (sourceIdParam) crawlerFilters.push(eq30(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
@@ -32818,7 +33641,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32818
33641
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
32819
33642
|
const userFetchFilters = [
|
|
32820
33643
|
eq30(aiUserFetchEventsHourly.projectId, project.id),
|
|
32821
|
-
|
|
33644
|
+
gte6(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
32822
33645
|
lte4(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
32823
33646
|
];
|
|
32824
33647
|
if (sourceIdParam) userFetchFilters.push(eq30(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
@@ -32843,7 +33666,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32843
33666
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
32844
33667
|
const aiFilters = [
|
|
32845
33668
|
eq30(aiReferralEventsHourly.projectId, project.id),
|
|
32846
|
-
|
|
33669
|
+
gte6(aiReferralEventsHourly.tsHour, sinceIso),
|
|
32847
33670
|
lte4(aiReferralEventsHourly.tsHour, untilIso)
|
|
32848
33671
|
];
|
|
32849
33672
|
if (sourceIdParam) aiFilters.push(eq30(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
@@ -34461,7 +35284,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
34461
35284
|
];
|
|
34462
35285
|
|
|
34463
35286
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
34464
|
-
import { and as and28, eq as eq36, gte as
|
|
35287
|
+
import { and as and28, eq as eq36, gte as gte7, ne as ne4, sql as sql13 } from "drizzle-orm";
|
|
34465
35288
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
34466
35289
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
34467
35290
|
function skippedNoProject5() {
|
|
@@ -34558,7 +35381,7 @@ var recentDataCheck = {
|
|
|
34558
35381
|
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
34559
35382
|
and28(
|
|
34560
35383
|
eq36(crawlerEventsHourly.projectId, ctx.project.id),
|
|
34561
|
-
|
|
35384
|
+
gte7(crawlerEventsHourly.tsHour, warnCutoff)
|
|
34562
35385
|
)
|
|
34563
35386
|
).get()?.total ?? 0
|
|
34564
35387
|
);
|
|
@@ -34566,7 +35389,7 @@ var recentDataCheck = {
|
|
|
34566
35389
|
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
34567
35390
|
and28(
|
|
34568
35391
|
eq36(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
34569
|
-
|
|
35392
|
+
gte7(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
34570
35393
|
)
|
|
34571
35394
|
).get()?.total ?? 0
|
|
34572
35395
|
);
|
|
@@ -34582,7 +35405,7 @@ var recentDataCheck = {
|
|
|
34582
35405
|
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
34583
35406
|
and28(
|
|
34584
35407
|
eq36(crawlerEventsHourly.projectId, ctx.project.id),
|
|
34585
|
-
|
|
35408
|
+
gte7(crawlerEventsHourly.tsHour, failCutoff)
|
|
34586
35409
|
)
|
|
34587
35410
|
).get()?.total ?? 0
|
|
34588
35411
|
);
|
|
@@ -34590,7 +35413,7 @@ var recentDataCheck = {
|
|
|
34590
35413
|
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
34591
35414
|
and28(
|
|
34592
35415
|
eq36(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
34593
|
-
|
|
35416
|
+
gte7(aiReferralEventsHourly.tsHour, failCutoff)
|
|
34594
35417
|
)
|
|
34595
35418
|
).get()?.total ?? 0
|
|
34596
35419
|
);
|
|
@@ -35005,7 +35828,7 @@ async function doctorRoutes(app, opts) {
|
|
|
35005
35828
|
|
|
35006
35829
|
// ../api-routes/src/discovery/routes.ts
|
|
35007
35830
|
import crypto28 from "crypto";
|
|
35008
|
-
import { and as and29, desc as desc16, eq as eq37, gte as
|
|
35831
|
+
import { and as and29, desc as desc16, eq as eq37, gte as gte8, inArray as inArray13, isNull, or as or5 } from "drizzle-orm";
|
|
35009
35832
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
35010
35833
|
async function discoveryRoutes(app, opts) {
|
|
35011
35834
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -35065,7 +35888,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
35065
35888
|
DiscoverySessionStatuses.seeding,
|
|
35066
35889
|
DiscoverySessionStatuses.probing
|
|
35067
35890
|
]),
|
|
35068
|
-
|
|
35891
|
+
gte8(discoverySessions.createdAt, ageFloorIso)
|
|
35069
35892
|
)).orderBy(desc16(discoverySessions.createdAt)).get();
|
|
35070
35893
|
if (existing && existing.runId) {
|
|
35071
35894
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
@@ -35685,7 +36508,7 @@ function dedupeStrings(input) {
|
|
|
35685
36508
|
|
|
35686
36509
|
// ../api-routes/src/technical-aeo.ts
|
|
35687
36510
|
import crypto30 from "crypto";
|
|
35688
|
-
import { and as and30, asc as
|
|
36511
|
+
import { and as and30, asc as asc7, count, desc as desc17, eq as eq39, inArray as inArray14, lt as lt2 } from "drizzle-orm";
|
|
35689
36512
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
35690
36513
|
function emptyScore(projectName) {
|
|
35691
36514
|
return {
|
|
@@ -35717,16 +36540,20 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
35717
36540
|
async function technicalAeoRoutes(app, opts) {
|
|
35718
36541
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
35719
36542
|
const project = resolveProject(app.db, request.params.name);
|
|
35720
|
-
const
|
|
36543
|
+
const baseFilters = [
|
|
35721
36544
|
eq39(siteAuditSnapshots.projectId, project.id),
|
|
35722
36545
|
eq39(runs.kind, RunKinds["site-audit"]),
|
|
35723
36546
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35724
36547
|
notProbeRun()
|
|
35725
|
-
|
|
35726
|
-
const
|
|
35727
|
-
|
|
36548
|
+
];
|
|
36549
|
+
const targetFilters = request.query.runId ? [...baseFilters, eq39(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
|
|
36550
|
+
const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
36551
|
+
if (!latest) {
|
|
36552
|
+
if (request.query.runId) throw notFound("Site audit run", request.query.runId);
|
|
36553
|
+
return emptyScore(project.name);
|
|
36554
|
+
}
|
|
35728
36555
|
const snap = latest.snap;
|
|
35729
|
-
const previous =
|
|
36556
|
+
const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(...baseFilters, lt2(siteAuditSnapshots.createdAt, snap.createdAt))).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get()?.snap ?? null;
|
|
35730
36557
|
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
35731
36558
|
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
35732
36559
|
return {
|
|
@@ -35752,12 +36579,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35752
36579
|
});
|
|
35753
36580
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
35754
36581
|
const project = resolveProject(app.db, request.params.name);
|
|
35755
|
-
const
|
|
36582
|
+
const targetFilters = [
|
|
35756
36583
|
eq39(siteAuditSnapshots.projectId, project.id),
|
|
35757
36584
|
eq39(runs.kind, RunKinds["site-audit"]),
|
|
35758
36585
|
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35759
36586
|
notProbeRun()
|
|
35760
|
-
|
|
36587
|
+
];
|
|
36588
|
+
if (request.query.runId) targetFilters.push(eq39(siteAuditSnapshots.runId, request.query.runId));
|
|
36589
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
36590
|
+
if (!latest && request.query.runId) {
|
|
36591
|
+
throw notFound("Site audit run", request.query.runId);
|
|
36592
|
+
}
|
|
35761
36593
|
if (!latest) {
|
|
35762
36594
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
35763
36595
|
}
|
|
@@ -35769,7 +36601,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35769
36601
|
const total = totalRow?.value ?? 0;
|
|
35770
36602
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
35771
36603
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
35772
|
-
const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ?
|
|
36604
|
+
const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc7(siteAuditPages.url) : asc7(siteAuditPages.overallScore);
|
|
35773
36605
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
35774
36606
|
const pages = rows.map((row) => ({
|
|
35775
36607
|
url: row.url,
|
|
@@ -35944,7 +36776,8 @@ async function apiRoutes(app, opts) {
|
|
|
35944
36776
|
await api.register(adsRoutes, {
|
|
35945
36777
|
adsCredentialStore: opts.adsCredentialStore,
|
|
35946
36778
|
verifyAdsAccount: opts.verifyAdsAccount,
|
|
35947
|
-
onAdsSyncRequested: opts.onAdsSyncRequested
|
|
36779
|
+
onAdsSyncRequested: opts.onAdsSyncRequested,
|
|
36780
|
+
adsOperator: opts.adsOperator
|
|
35948
36781
|
});
|
|
35949
36782
|
await api.register(bingRoutes, {
|
|
35950
36783
|
bingConnectionStore: opts.bingConnectionStore,
|
|
@@ -36492,7 +37325,7 @@ var IntelligenceService = class {
|
|
|
36492
37325
|
const selected = this.db.select().from(gbpLocations).where(and31(
|
|
36493
37326
|
eq40(gbpLocations.projectId, projectId),
|
|
36494
37327
|
eq40(gbpLocations.selected, true),
|
|
36495
|
-
|
|
37328
|
+
gte9(gbpLocations.syncedAt, windowStart),
|
|
36496
37329
|
lte5(gbpLocations.syncedAt, windowEnd)
|
|
36497
37330
|
)).all();
|
|
36498
37331
|
if (selected.length === 0) {
|
|
@@ -36706,7 +37539,7 @@ var IntelligenceService = class {
|
|
|
36706
37539
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
36707
37540
|
ne5(runs.trigger, RunTriggers.probe)
|
|
36708
37541
|
)
|
|
36709
|
-
).orderBy(
|
|
37542
|
+
).orderBy(asc8(runs.finishedAt)).all();
|
|
36710
37543
|
let startIdx = 0;
|
|
36711
37544
|
let endIdx = allRuns.length;
|
|
36712
37545
|
if (opts?.fromRunId) {
|